تست برنامه‌های جنگوی شما با pytest

امیرحسین بیگدلو 12 ماه قبل

بسیاری از برنامه‌نویسان جامعه پایتون، درباره تست واحد (unit testing) شنیده‌اند و از آن برای تست پروژه‌هایشان استفاده می‌کنند، و از حجم کدهای بویلرپلیت (boilerplate code: کدی که ممکن است در زبانی با تعداد خط کمتری پیاده‌سازی شود) ماژول‌های تست واحد پایتون و جنگو آگاهند. اما پایتست (pytest)، تست‌هایی پایتون-مانندتر با کد بویلرپلیت کم‌تری را به ما ارائه می‌کند.

 

 

چرا باید از pytest استفاده کنید؟

پایتست روشی جدید برای نوشتن تست‌ها ارائه می‌دهد؛ تست‌های تابعی (functional) برای برنامه‌ها و کتابخانه‌ها. در زیر، مزایا و معایب این فریمورک را لیست می‌کنم:

 

 

مزایای استفاده از pytest:

  •     عبارات اظهار (Assert) (نیاز به یادآوری نام‌های self.assert نیست)
  •     توضیحات دقیق درباره خطاها
  •     پایه تست‌ها (دقیق، ماژول‌محور و گسترش‌پذیر)
  •     ویژگی‌های دیگر پایه‌تست‌ها (استفاده خودکار، scope (بازه)، شیء درخواست‌دهنده، تودرتو بودن، نهایی‌کننده‌ها و...)
  •     شناسایی خودکار تابع‌ها و ماژول‌های تست
  •     نشانه‌ها
  •     پارامترگذاری
  •     حجم کد بویلرپلیت کم‌تر: تنها نیاز است فایلی ایجاد کرده، تابعی با assert بنویسید و آن را اجرا کنید (سادگی بهتر از پیچیدگی است!)
  •     برخلاف PyUnit، نیازی به سیستم نام‌گذاری CamelCase نیست.
  •     بیش از 736 افزونه و جامعه کاربری فعال
  •     می‌تواند به راحتی تست‌های nose و unittest را اجرا کند.
  •     پشتیبانی از پایتون 3.5+ و PyPy 3

 

 

معایب استفاده از pytest:

 

  •     به نسبت استفاده از unittest، به دانش عمیق‌تری از پایتون نیاز دارد؛ مانند استفاده از decorator و simple generatorها.
  •     نیاز به نصب جداگانه ماژول. اما می‌تواند جزء مزایا نیز در نظر گرفته شود، زیرا وابستگی به نسخه پایتون وجود نخواهد داشت. اگر به ویژگی‌های جدید نیاز دارید، تنها باید پکیج پایتست را به روزرسانی کنید.

 


 

مقدمه‌ای کوتاه درباره pytest

 

ابتدا قصد دارم مقدار مختصری به بحث فلسفه pytest و سینتکس پایه آن ورود کنم. این کار را در قالب پاسخ به سوال‌های رایج انجام می‌دهم. این بخش مقدمه‌ای بسیار کوتاه درباره پایتست و کاربردهای پایه آن است، اما حتما مطالعه‌اش کنید، زیرا در بخش‌های بعدی از این بخش استفاده خواهیم کرد.  

 

 

1. پایه‌تست‌های pytest چیستند؟

پایه‌تست‌ها توابعی هستند که قبل و بعد از هر تست اجرا می‌شوند؛ مانند setup و teardown در unitest و ویژگی pytest killer برچسب‌گذاری‌شده. پایه‌تست‌ها برای پیکربندی داده‌ها، اتصال و عدم اتصال پایگاه داده، فراخوانی عملیات‌های اضافه و... استفاده می‌شوند.  

همه پایه‌تست‌ها دارای آرگومان‌ scope با مقادیر در دسترس زیر هستند:

  •     function به ازای هر تست یک بار اجرا می‌شود.
  •     class به ازای هر کلاس از تست‌ها یک بار اجرا می‌شود.
  •     module به ازای هر ماژول یک بار اجرا می‌شود.
  •     session به ازای هر نشست یک بار اجرا می‌شود.

 

نکته: مقدار پیش‌فرض آرگومان scope، function است.

 

مثالی از ایجاد یک پایه‌تست ساده:

import pytest


@pytest.fixture
def function_fixture():
   print('Fixture for each test')
   return 1


@pytest.fixture(scope='module')
def module_fixture():
   print('Fixture for module')
   return 2

 

نوع دیگری از پایه‌تست، پایه‌تست بازده است که امکان دسترسی به تست قبل و بعد از اجرا را فراهم می‌کند؛ مانند setup و teardown.

 

مثالی از ایجاد یک پایه‌تست بازده ساده:

import pytest

@pytest.fixture
def simple_yield_fixture():
   print('setUp part')
   yield 3
   print('tearDown part')

 

نکته

پایه‌تست‌های عادی می‌توانند از yield مستقیما استفاده کنند. در این صورت دیگر به yield_fixture نیازی نخواهد بود.

 

 

2. چگونه پایه‌تست‌ها را در تست با پایتست استفاده کنیم؟

برای استفاده از آنها در تست، می‌توانید نام پایه‌تست را به عنوان آرگومان تابع وارد کنید.

 


def test_function_fixture(function_fixture):
  assert function_fixture == 1

def test_yield_fixture(simple_yield_fixture):
  assert simple_yield_fixture == 3

 

نکته

پایتست به طور خودکار پایه‌تست‌ها را ثبت کرده و با استفاده از مکانیزم import خارجی، به آنان دسترسی پیدا می‌کند.

 

 

3. نشانه‌ها (marks) در پایتست چه هستند؟

 

نشانه‌ها در تنظیم مِتادیتاها در توابع تست به ما کمک می‌کنند. برای نمونه:

 

  •     skip: همیشه یک تابع تست را رد می‌کند.
  •     xfail: اگر شرط معینی اجرا شود، خروجی "خطای غیرمنتظره" را تولید می‌کند.

 

مثالی از نشانه‌ها:

import pytest


@pytest.mark.xfail
def test_some_magic_test():
   ...

    
@pytest.mark.skip
def test_old_functional():
   ...

 

 

4. چگونه برای پایتست نشانه‌های شخصی درست کنیم؟

 

تنها راه، تعریف نشانه‌ها در فایل pytest.ini است:

[pytest]
markers =
   slow: marks tests as slow
   serial

 

نکته: هر چیزی که بعد از : نوشته شده، توضیحی اختیاری برای نشانه است.

 

 

5. چگونه تست‌ها را با نشانه‌ها در پایتست اجرا کنیم؟

 

می‌توانید با استفاده از دستور بعدی، با استفاده از xfail و بدون کم کردن سرعت نشانه‌ها، این کار را انجام دهید:

pytest -m "xfail and not slow" --strict-markers

 

نکته

زمانی که از --strict-markers استفاده می‌شود، هر نشانه ناشناس با ساختار @pytest.mark.name_of_the_mark سبب بروز خطا خواهد شد.

 

 

6. پارامترایز در پایتست چیست؟

 

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

مثال ساده‌ای از استفاده پارامترایز در تست:

import pytest

@pytest.mark.parametrize(
   'text_input, result', [('5+5', 10), ('1+4', 5)]
)
def test_sum(text_input, result):
   assert eval(text_input) == result

 

 

در اینجا سوالات به اتمام می‌رسند. در ادامه با این مفاهیم پایه کار خواهیم کرد تا پایتست را برای پروژه جنگو شما تنظیم کنیم.

 


 

تنظیم pytest برای پروژه‌های جنگوی شما

 

برای تست پروژه‌های جنگو با پایتست، از اول شروع نکرده و از افزونه pytest-django استفاده خواهیم کرد، که مجموعه‌ای از ابزارهای کاربردی برای تست برنامه‌ها و پروژه‌های جنگو را برای ما فراهم می‌آورد. بیایید با نصب افزونه شروع کنیم.

 

 

1. نصب

پایتست می‌تواند به وسیله pip نصب شود:

pip install pytest-django

نصب pytest-django به طور خودکار سبب نصب آخرین نسخه پایتست می‌شود. pytest-django از سیستم افزونه پایتست استفاده می‌کند و بعد از نصب، مستقیما قابل استفاده بوده و نیاز به تنظیمات بیش‌تری ندارد.

 

 

2. تنظیمات جنگو را مبتنی بر پایتست کنید

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

 

در فولدر مادر پروژه خود، فایلی به نام pytest.ini ایجاد کنید که شامل موارد زیر است:

 

[pytest]
DJANGO_SETTINGS_MODULE = yourproject.settings

 

همچنین می‌توانید تنظیمات جنگو را با تنظیم متغیر محیطی DJANGO_SETTINGS_MODULE، یا مشخص کردن نشانه خط فرمان --ds=yourproject.settings در زمان اجرای تست‌ها نیز تغییر دهید.

 

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

 

[pytest]
DJANGO_SETTINGS_MODULE = yourproject.settings
python_files = tests.py test_*.py *_tests.py

 

 

3. مجموعه تست خود را اجرا کنید

تست‌ها برخلاف فرمان manage.py test که ممکن است به آن عادت کرده باشید، با فرمان pytest به طور خودکار فعال می‌شوند.

pytest

یک فایل تست یا فولدر خاص می‌توانند با مشخص کردن نام آنها در فرمان اجرا شوند:

pytest a_directory                     # directory
pytest test_something.py               # tests file
pytest test_something.py::single_test  # single test function

 

نکته: ممکن است فکر کنید، "چرا باید از این فرمان به جای فرمان manage.py test جنگو استفاده کنم"؟ خیلی آسان است. اجرای مجموعه تست با pytest ویژگی‌هایی را ارائه می‌کند که در مکانیزم تست استاندارد جنگو موجود نیستند:

 

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

 

اکنون، آماده هستیم که اولین تست را با استفاده از پایتست و جنگو بنویسیم.

 


 

تست جنگو با pytest

 

1. راهنماهای پایگاه‌داده

برای دستیابی به پایگاه داده pytest-django، از نشانه django_db استفاده کرده یا یکی از پایه‌تست‌های db، transactional_db یا django_db_reset_sequences را درخواست دهید.

 

نکته

تمامی متدهای دسترسی پایگاه‌داده‌ها به طور خودکار از django.test.TestCase استفاده می‌کنند.

 

 

django_db: برای دسترسی به پایگاه‌داده جنگو، هر تست در تراکنش خود اجرا شده و در انتهای تست بازگردانده می‌شود؛ همان اتفاقی که در django.test.TestCase می‌افتد. ما دائما از آن استفاده می‌کنیم، زیرا جنگو به دسترسی به پایگاه‌داده نیاز دارد.

 

import pytest

from django.contrib.auth.models import User


@pytest.mark.django_db
def test_user_create():
  User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
  assert User.objects.count() == 1

 

اگر می‌خواهید داخل یک پایه‌تست به پایگاه‌داده جنگو دسترسی پیدا کنید، حتی اگر تابع درخواست‌دهنده پایه‌تست این نشانه را داشته باشد، کمکی نخواهد کرد. برای دسترسی به پایگاه‌داده از یک پایه‌تست، پایه‌تست باید به یکی از پایه‌تست‌های db، transactional_db یا django_db_reset_sequences درخواست دهد. در زیر تعریفی برای هر کدام می‌خوانید:

 

db: این پایه‌تست اطمینان حاصل می‌کند که پایگاه‌داده جنگو تنظیم شده است و تنها برای پایه‌تست‌هاییست که می‌خواهند خودشان از پایگاه‌داده استفاده کنند. یک تابع تست معمولا باید از نشانه pytest.mark.django_db برای اعلام نیاز به اتصال به پایگاه‌داده استفاده کند.

 

transactional_db: این پایه‌تست برای درخواست دسترسی به پایگاه‌داده به همراه پشتیبانی تراکنش استفاده می‌شود و برای پایه‌تست‎‌هایی استفاده می‌شود که خود به دسترسی به پایگاه‌داده نیاز دارند. یک تابع تست معمولا باید از نشانه pytest.mark.django_db به همراه transaction=True استفاده کند.

 

django_db_reset_sequences: این پایه‌تست دسترسی تراکنشی همانند transactional_db، به همراه پشتیبانی از بازنشانی خودکار دنباله‌های افزایشی را ارائه می‌دهد و تنها برای پایه‌تست‌هاییست که می‌خواهند خودشان از پایگاه‌داده استفاده کنند. یک تابع تست معمولا باید از نشانه pytest.mark.django_db به همراه transaction=True و reset_sequences=True استفاده کند.

 

 

2. کاربر (Client)

django.test.clientبه وفور در تست واحد جنگو استفاده می‌شود، زیرا آن را برای هر درخواستمان به برنامه استفاده می‌کنیم. پایتست نیز دارای یک پایه‌تست client است:

 

import pytest

from django.urls import reverse

@pytest.mark.django_db
def test_view(client):
   url = reverse('homepage-url')
   response = client.get(url)
   assert response.status_code == 200

 

3. کاربر ادمین

برای به دست‌آوردن دسترسی فراکاربری، می‌توانیم از admin_client استفاده کنیم:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_unauthorized(client):
   url = reverse('superuser-url')
   response = client.get(url)
   assert response.status_code == 401


@pytest.mark.django_db
def test_superuser_view(admin_client):
   url = reverse('superuser-url')
   response = admin_client.get(url)
   assert response.status_code == 200

 

 

4. ایجاد پایه‌تست کاربر

 

در جهت ایجاد کاربر برای تست، دو راه داریم:

 

1) استفاده از پایه‌تست‌های جنگویِ پایتست:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_user_detail(client, django_user_model):
   user = django_user_model.objects.create(
       username='someone', password='password'
   )
   url = reverse('user-detail-view', kwargs={'pk': user.pk})
   response = client.get(url)
   assert response.status_code == 200
   assert 'someone' in response.content

 

django_user_model: راهنمای pytest-djangoبرای دسترسی راحت به مدل کاربر که توسط پروژه چنگو فعلی تنظیم شده است، مانند settings.AUTH_USER_MODEL.

 

معایب این روش:

  •     باید برای هر تست کپی شود.
  •     اجازه تعریف فیلدهای تفاوت را نمیدهد، زیرا پایه‌تست‌ها به جای ما از کاربر نمونه (instance) ایجاد می‌کنند.

 

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_superuser_detail(client, admin_user):
   url = reverse(
       'superuser-detail-view', kwargs={'pk': admin_user.pk}
   )
   response = client.get(url)
   assert response.status_code == 200
   assert 'admin' in response.content

 

admin_user: راهنمای pytest-django که به جای فراکاربر استفاده می‌شود و نام کاربری آن “admin” و رمز عبور آن “password” است (در صورتی که هنوز کاربر ادمین تعریف نشده باشد).

 

 

2) ایجاد پایه‌تست شخصی:

برای رفع ایرادات بالا، پایه‌تست شخصی خود را ایجاد می‌کنیم:

import uuid

import pytest


@pytest.fixture
def test_password():
   return 'strong-test-pass'

  
@pytest.fixture
def create_user(db, django_user_model, test_password):
   def make_user(**kwargs):
       kwargs['password'] = test_password
       if 'username' not in kwargs:
           kwargs['username'] = str(uuid.uuid4())
       return django_user_model.objects.create_user(**kwargs)
   return make_user

 

نکته

از آنجا که پایه‌تست‌های پایتست آرگومان‌ها را نمی‌پذیرند، کاربر را با امکان فراخوانی توابع داخلی برای انتقال آرگومان‌ها به عنوان kwarg ایجاد کنید.

 

تست‌های بالا را بازنویسی می‌کنیم:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_user_detail(client, create_user):
   user = create_user(username='someone')
   url = reverse('user-detail-view', kwargs={'pk': user.pk})
   response = client.get(url)
   assert response.status_code == 200
   assert 'someone' in response.content


@pytest.mark.django_db
def test_superuser_detail(client, create_user):
   admin_user = create_user(
       username='custom-admin-name',
       is_staff=True, is_superuser=True
   )
   url = reverse(
       'superuser-detail-view', kwargs={'pk': admin_user.pk}
   )
   response = client.get(url)
   assert response.status_code == 200
   assert 'custom-admin-name' in response.content

 

create_user: می‌توانیم این مثال را با پایه‌تست‌هایی مانند create_base_user (مانند کاربر پایه) و create_superuser (برای فراکاربر) گسترش دهیم.

 

 

5. ورود خودکار کاربر

بیایید یک کاربر تاییدشده را تست کنیم:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_auth_view(client, create_user, test_password):
   user = create_user()
   url = reverse('auth-url')
   client.login(
       username=user.username, password=test_password
   )
   response = client.get(url)
   assert response.status_code == 200

 

ایراد بزرگ این روش این است که بلوک کد ورود باید برای هر تست تکرار شود.

 

بیایید پایه‌تست خود را برای ورود خودکار کاربر ایجاد کنیم:

import pytest


@pytest.fixture
def auto_login_user(db, client, create_user, test_password):
   def make_auto_login(user=None):
       if user is None:
           user = create_user()
       client.login(username=user.username, password=test_password)
       return client, user
   return make_auto_login

 

auto_login_user: پایه‌تست ما کاربر را به عنوان پارامتر می‌گیرد یا کاربر جدیدی ایجاد می‌کند و در پایه‌تست client وارد می‌کند. در آخر هم کاربر و هم client برای عملیات‌های بعدی برگردانده می‌شوند.

 

پایه‌تست جدید را برای تست بالا اجرا می‌کنیم:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_auth_view(auto_login_user):
   client, user = auto_login_user()
   url = reverse('auth-url')
   response = client.get(url)
   assert response.status_code == 200

 

 

6. پارامترسازی تست با pytest

 فرض کنید میخواهیم چند تست بسیار مشابه اجرا کنیم، مثلا تست‌هایی در مورد زبان‌های مختلف. پیش از این لازم بود تست‌های جدا از هم بنویسیم، مثلا:

def test_de_language():
   ...
def test_gr_language():
   ...
def test_en_language():
   ...

 

 

    اینکه کد را کپی کنیم بامزه است، اما در طولانی‌مدت، نه! (آندرو اسولتوف)

 

 

برای رفع این مشکل، پایتست دارای ویژگی پارامترسازی پایه‌تست‌هاست. در اینجا تست‌های زیر را داریم:

import pytest

from django.urls import reverse


@pytest.mark.django_db
@pytest.mark.parametrize([
   ('gr', 'Yasou'),
   ('de', 'Guten tag'),
   ('fr', 'Bonjour')
])
def test_languages(language_code, text, client):
   url = reverse('say-hello-url')
   response = client.get(
       url, data={'language_code': language_code}
   )
   assert response.status_code == 200
   assert text == response.content

می‌توانید ببینید چقدر آسان‌تر و کم‌حجم‌تر شد!

 

 

7. تست Outbox ایمیل با pytest

برای تست outbox ایمیل، pytest-django یک افزونه داخلی به نام mailoutbox دارد:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_send_report(auto_login_user, mailoutbox):
   client, user = auto_login_user()
   url = reverse('send-report-url')
   response = client.post(url)
   assert response.status_code == 201
   assert len(mailoutbox) == 1
   mail = mailoutbox[0]
   assert mail.subject == f'Report to {user.email}'
   assert list(mail.to) == [user.email]

 

برای این تست از پایه‌تست auto_login_user خودمان و mailoutbox استفاده می‌کنیم.

 

به طور خلاصه، مزایای روش بالا بدین صورت است: پایتست به ما یاد می‌دهد چگونه تست‌هایمان را به راحتی تنظیم کنیم تا بتوانیم تمرکز بیشتری بر کاربرد اصلی تست‌هایمان داشته‌باشیم.

 


 

تست فریمورک REST جنگو با pytest

 

1. API Client:

اولین کاری که باید انجام دهیم، ایجاد یک پایه‌تست شخصی برای API Client فریمورک REST است:

import pytest


@pytest.fixture
def api_client():
   from rest_framework.test import APIClient
   return APIClient()

 

اکنون api_client را برای تست‌هایمان داریم:

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_unauthorized_request(api_client):
   url = reverse('need-token-url')
   response = api_client.get(url)
   assert response.status_code == 401

 

 

2. گرفتن یا ایجاد توکن (Token)

برای اعتبارسنجی، users در API معمولا از توکن استفاده می‌کنند. بیایید پایه‌تستی بنویسیم که توکن ایجاد کرده یا آن را از کاربر می‌گیرد:

import pytest

from rest_framework.authtoken.models import Token


@pytest.fixture
def get_or_create_token(db, create_user):
   user = create_user()
   token, _ = Token.objects.get_or_create(user=user)
   return token

 

get_or_create_token: وراثت create_user

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_unauthorized_request(api_client, get_or_create_token):
   url = reverse('need-token-url')
   token = get_or_create_token()
   api_client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
   response = api_client.get(url)
   assert response.status_code == 200

 

3. اعتبارسنجی خودکار

تست بالا مثال خوبیست، اما اعتبارسنجی این تست‌ها سبب تولید کد بویلرپلیت خواهد شد. می‌توانیم از متد APIClient برای دور زدن روند اعتبارسنجی استفاده کنیم.

 

از ویژگی yield برای گسترش پایه‌تست جدید استفاده می‌کنیم:

import pytest


@pytest.fixture
def api_client_with_credentials(
   db, create_user, api_client
):
   user = create_user()
   api_client.force_authenticate(user=user)
   yield api_client
   api_client.force_authenticate(user=None)

 

api_client_with_credentials: وراثت create_user و api_client، مقادیر اعتبارسنجی را پس از هر تست پاک می‌کند.

 

import pytest

from django.urls import reverse


@pytest.mark.django_db
def test_authorized_request(api_client_with_credentials):
   url = reverse('need-auth-url')
   response = api_client_with_credentials.get(url)
   assert response.status_code == 200

 

 

4. اعتبارسنجی داده با پارامترسازی pytest

بیشتر تست‌های API endpoint متمرکز بر اعتبارسنجی داده هستند. باید همان تست‌ها را بدون شمارش تفاوت در چندین مقادیر ایجاد کنید. می‌توانیم از پایه‌تست parametrizing fixture برای این کار استفاده کنیم:

import pytest


@pytest.mark.django_db
@pytest.mark.parametrize(
   'email, password, status_code', [
       (None, None, 400),
       (None, 'strong_pass', 400),
       ('user@example.com', None, 400),
       ('user@example.com', 'invalid_pass', 400),
       ('user@example.com, 'strong_pass', 201),
   ]
)
def test_login_data_validation(
   email, password, status_code, api_client
):
   url = reverse('login-url')
   data = {
       'email': email,
       'password': password
   }
   response = api_client.post(url, data=data)
   assert response.status_code == status_code

 

با این روش، به علت استفاده از این ویژگی pytest، چندین حالت را با یک تابع تست بررسی می‌کنیم.

 

 

5. ماک (mock) کردن عملیات‌های اضافه در view های شما

 

بیایید ببینیم ‘unittest.mock’ چه‌طور می‌تواند در تست‌های ما ایجاد شود. ترجیح میدهم به جای پایه‌تست ‘monkeypatch’ از ‘unittest.mock’ استفاده کنیم. می‌توانید از پکیج pytest-mock نیز استفاده کنید، زیرا متدهای مفید بسیاری دارد، مانند: assert_called_once() ، assert_called_with(*args,**kwargs)، assert_called()  و assert_not_called().

 

اگر می‌خواهید از پایه‌تست monkeypatch استفاده کنید، بهتر است صفحه رسمی آن را مطالعه کنید.

 

برای مثال، در اینجا پس از ذخیره داده، یک فراخوانی سرویس سوم داریم:

 

from rest_framework import generics

from .services import ThirdPartyService


class CreateEventView(generics.CreateAPIView):
   ...
def perform_create(self, serializer):
       event= serializer.save()
       ThirdPartyService.send_new_event(event_id=event.id)

 

می‌خواهیم endpoint را بدون درخواست اضافه به سرویس امتحان کرده و از mock.patch استفاده کنیم:

 

import pytest

from unittest import mock


@pytest.fixture
def default_event_data():
   return {'name': 'Wizz Marathon 2019', 'event-type': 'sport'}


@pytest.mark.django_db
@mock.patch('service.ThirdPartyService.send_new_event')
def test_send_new_event_service_called(
   mock_send_new_event, default_event_data, api_client
):
   response = api_client.post(
       'create-service', data=default_event_data
   )
   assert response.status_code == 201
   assert response.data['id']
   mock_send_new_event.assert_called_with(
       event_id=response.data['id']
   )

 


 

نکات مفید درباره pytest

 

1. استفاده از Factory Boy به جای پایه‌تست‌ها برای تست مدل جنگو

روش‌های بسیاری برای ایجاد نمونه مدل‌های جنگو و تست آنها با پایه‌تست‌ها وجود دارد.

 

  •     ایجاد شیء به طور دستی: روش سنتی : "ایجاد دستی داده‌های تست و پشتیبانی از آن".
import pytest


from django.contrib.auth.models import User
@pytest.fixture
def user_fixture(db):
   return User.objects.create_user(
       'john', 'lennon@thebeatles.com', 'johnpassword'
   )

 

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

 

import pytest

from django.contrib.auth.models import User, Group


@pytest.fixture
def default_group_fixture(db):
   default_group, _ = Group.objects.get_or_create(name='default')
   return default_group

@pytest.fixture
def user_with_default_group_fixture(db, default_group_fixture):
   user = User.objects.create_user(
       'john', 'lennon@thebeatles.com', 'johnpassword',
       groups=[default_group_fixture]
   )
   return user

 

  •     پایه‌تست‌های جنگو: کند و سخت برای کار کردن ... از آنها دوری کنید!

 

در ایمجا مثالی برای مقایسه آورده‌ام:

[
 {
 "model": "auth.group",
 "fields": {
   "name": "default",
   "permissions": [
     29,45,46,47,48
   ]
 }
},
{
 "model": "auth.user",
 "pk": 1,
 "fields": {
   "username": "simple_user",
   "first_name": "John",
   "last_name": "Lennon",
   "groups": [1],
 }
},
// create permissions here
]

 

 

    Factoryها: روشی آسان برای ایجاد داده‌های تست شما. من ترجیح می‌دهم با پایتست از pytest-factoryboy و factoryboy استفاده کنم، اما model mommy هم قابل استفاده است.

 

1-     افزونه را نصب کنید:

pip install pytest-factoryboy

 

2-     User Factory ایجاد کنید:

import factory

from django.contrib.auth.models import User, Group


class UserFactory(factory.DjangoModelFactory):
  class Meta:
       model = User

   username = factory.Sequence(lambda n: f'john{n}')
   email = factory.Sequence(lambda n: f'lennon{n}@thebeatles.com')
   password = factory.PostGenerationMethodCall(
       'set_password', 'johnpassword'
   )

   @factory.post_generation
   def has_default_group(self, create, extracted, **kwargs):
       if not create:
           return
       if extracted:
           default_group, _ = Group.objects.get_or_create(
               name='group'
           )
           self.groups.add(default_group)

 

3- سپس آن را ثبت کنید:

from pytest_factoryboy import register

from factories import UserFactory


register(UserFactory)  # name of fixture is user_factory

 

نکته

نام گذاری بهتر است با استفاده از حروف کوچک و – باشد.

 

4- Factory را تست کنید:

import pytest


@pytest.mark.django_db
def test_user_user_factory(user_factory):
   user = user_factory(has_default_group=True)
   assert user.username == 'john0'
   assert user.email == 'lennon0@thebeatles.com'
   assert user.check_password('johnpassword')
   assert user.groups.count() == 1

 

 

2. بهبود تست‌های پارامتر سازنده شما

 

بیایید این تست‌ها را با برخی ویژگی‌ها، بهبود بخشیم:

 

import pytest


@pytest.mark.django_db
@pytest.mark.parametrize(
   'email, password, status_code', [
       ('user@example.com', 'invalid_pass', 400),
       pytest.param(
           None, None, 400,
           marks=pytest.mark.bad_request
       ),
       pytest.param(
           None, 'strong_pass', 400,
           marks=pytest.mark.bad_request,
           id='bad_request_with_pass'
       ),
       pytest.param(
           'some@magic.email', None, 400,
           marks=[
               pytest.mark.bad_request,
               pytest.mark.xfail
           ],
           id='incomprehensible_behavior'
       ),
       pytest.param(
           'user@example.com', 'strong_pass', 201,
           marks=pytest.mark.success_request
       ),
   ]
)
def test_login_data_validation(
   email, password, status_code, api_client
):
   url = reverse('login-url')
   data = {
       'email': email,
       'password': password
   }
   response = api_client.post(url, data=data)
   assert response.status_code == status_code

 

  • pytest.param: شیء pytest برای اضافه کردن آرگومان‌های اضافه مانند مانند نشانه و شناسه‌ها.
  • marks: آرگومان تنظیم کننده نشانه پایتست.
  • id: آرگومانی برای تخصیص شناسه‌ای یکتا به هر تست.
  • success_request و bad_request: نشانه‌های پایتست.

 

بیایید تست را با چند شرط اجرا کنیم:

 

pytest -m bad_request

============== test session starts =================
collecting ... collected 5 items / 2 deselected / 3 selected
test_login.py::test_login_data_validation[None-None-400] PASSED              [ 33%]
test_login.py::test_login_data_validation[bad_request_with_pass] PASSED      [ 66%]
test_login.py::test_login_data_validation[incomprehensible_behavior] XFAIL   [100%]

 

 

در نتیجه:

  •     یک تست مجموع با نشانه bad_requests داریم
  •     تست‌هایی که فاقد شیء  pytest.param هستند را نادیده می‌گیریم، زیرا دارای پارمتر نشانه نیستند.
  •     تست‌ها با شناسه در کنسول نمایش داده می‌شوند.

 

 

3. ماک کردن تست با پایه‌تست‌ها

استفاده از pytest-mock روشی دیگر برای ماک کردن کد شما با روش پایتست، مبتنی بر نام‌گذاری پایه‌تست‌ها به عنوان پارامترهاست.

 

1- افزونه را نصب کنید:

pip install pytest-mock

 

2- مثال بالا را بازنویسی کنید:

import pytest


@pytest.mark.django_db
def test_send_new_event_service_called(
   mocker, default_event_data, api_client
):
   mock_send_new_event = mocker.patch(
       'service.ThirdPartyService.send_new_event'
   )
   response = api_client.post(
       'create-service', data=default_event_data
   )

   assert response.status_code == 201
   assert response.data['id']
   mock_send_new_event.assert_called_with(
       event_id=response.data['id']
   )

 

این ماکر، پایه‌تستیست که API مشابه با  mock.patch داشته و متدهای مشابهی را پشتیبانی می‌کند، مانند:

  • mocker.patch
  • mocker.patch.object
  • mocker.patch.multiple
  • mocker.patch.dict
  • mocker.stopall

 

مطالب مشابه



مونگارد