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

May 2021


[pytest] به شما کمک می‌کند برنامه‌های بهتری بنویسید.   -pytest

 

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

 

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

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

 

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

 

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

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

 

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

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

 

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

fixtureها توابعی هستند که قبل و بعد از هر تست اجرا می‌شوند؛ مانند 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

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

 

 

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

برای تست پروژه‌های جنگو با پایتست، از اول شروع نکرده و از افزونه 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 ویژگی‌هایی را ارائه می‌کند که در مکانیزم تست استاندارد جنگو موجود نیستند:

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

 

تست جنگو با 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. ایجاد پایه‌تست(fixture) User

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

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.

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

 

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. پارامترسازی تست با پایتست

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

...
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 ایمیل با پایتست

برای تست 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:

اولین کاری که باید انجام دهیم، ایجاد یک پایه‌تست(fixture) شخصی برای 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 به جای پایه‌تست‌ها برای تست مدل جنگو

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

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

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

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

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

[
 {
 "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
]

fixtureهایی ایجاد کنید که اطلاعات مربوط به fixtureهای دیگر را در session شما لود کنند.

import pytest

from django.core.management import call_command


@pytest.fixture(scope='session')
def django_db_setup(django_db_setup, django_db_blocker):
   with django_db_blocker.unblock():
       call_command('loaddata', ‘fixture.json')

 

Factory  .cها: روشی آسان برای ایجاد داده‌های تست شما. من ترجیح می‌دهم با پایتست از 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%]

در نتیجه:

 

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

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

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

pip install pytest-mock

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

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

 

4. اجرای همزمان تست‌ها

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

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

pip install pytest-xdist

    اجرای تست به صورت چندپردازشی:

pytest -n <number_of_processes>

نکته:

 

5. تنظیم فایل pytest.ini

مثالی از فایل pytest.ini داخل پروژه شما:

[pytest]
DJANGO_SETTINGS_MODULE = yourproject.settings
python_files = tests.py test_*.py *_tests.py
addopts = -p no:warnings --strict-markers --no-migrations --reuse-db
norecursedirs = venv old_tests
markers =
   custom_mark: some information of your mark
   slow: another one slow tes

DJANGO_SETTINGS_MODULE و python_files بالاتر بررسی شده‌اند، پس بیایید به بررسی ویژگی‌های کاربردی دیگر بپردازیم:

 

addopts:

OPTSهای مشخص‌شده را مانند یک کاربر به آرگومان‌های خط کاربری اضافه می‌کند. گزینه‌های بعدی را مشخص کرده‌ایم:

    --p no:warnings : گرفتن هشدارها را به طور کامل غیرفعال می‌کند (اگر مجموعه تست شما هشدارها را با یک سیستم خارجی مدیریت می‌کند، مناسب است).

    --strict-markers : خطاهای تایپی و تکرار در توابع به عنوان خطا در نظر گرفته می‌شوند.

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

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

مثالی از جریان کار (workflow) با --reuse-db و --create-db:

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

    زمانی که شمای پایگاه‌داده را تغییر می‌دهید،  pytest --create-db را اجرا کنید تا پایگاه‌داده تست مجددا ایجاد شود.

 

    norecursedirs:

مشخص می‌کند که پایتست در حال جستجو برای تست‌ها باید کدام الگوهای نام فایل را نادیده بگیرد. این به پایتست می‌گوید که venv و  old_testsdirectory  را بررسی نکند.  

نکته: الگوهای پیش فرض موارد زیر هستند:

'.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'

    نشانه‌‌گذارها

می‌توانید نشانه‌های اضافی در تنظیمات تعریف کنید تا به لیست سفید اضافه شده و در تست‌ها استفاده شوند.  

اجرای همه تست‌ها با نشانه slow:

pytest -m slow

 

6. نمایش میزان پوشش تست

برای نمایش میزان پوشش برنامه توسط تست از افزونه pytest-cov استفاده کنید:

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

pip install pytest-cov

    میزان پوشش پروژه و نمونه گزارش:

pytest --cov

-------------------- coverage: ... ---------------------
Name                 Stmts   Miss  Cover
----------------------------------------
proj/__init__          2      0    100%
proj/apps              257    13   94%
proj/proj              94     7    92%
----------------------------------------
TOTAL                  353    20   94%

 

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

مقالات مرتبط

django persian urls

صفر تا صد برنامه نویسی پایتون

چرا نرم‌افزارهای open source میسازیم؟‍

django image upload