Pytest Django¶
pytest-django jest dodatkiem do pytest, udostępniającą zestaw przydatnych narzędzi
do testowania aplikacji i projektów Django. Bez tego dodatku bardzo ciężko będzie nam
przetestować architekturę django bez wykorzystywania mock.
Instalacja¶
$ pip install pytest-django
Konfiguracja¶
W katalogu głównym naszej aplikacji w pliku pytest.ini dodajemy zmienną
DJANGO_SETTINGS_MODULE w której ustawiamy śieżkę do ustawień django.
[pytest]
DJANGO_SETTINGS_MODULE=config.settings.test
Istnieją jeszcze inne sposoby konfiguracji których opis można znaleść pod adresem: http://pytest-django.readthedocs.io/en/latest/configuring_django.html
Uruchomienie testów¶
Testy uruchamiamy w taki sam sposób jak testy w pytest. Mamy natomiast możliwość
uruchomienia manage.py test a w tle uruchamiamy pytest. Aby tego dokonać tworzymy
plik runner.py i w środku zamiszczamy poniższy kod:
class PytestTestRunner(object):
"""Runs pytest to discover and run tests."""
def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs):
self.verbosity = verbosity
self.failfast = failfast
self.keepdb = keepdb
def run_tests(self, test_labels):
"""Run pytest and return the exitcode.
It translates some of Django's test command option to pytest's.
"""
import pytest
argv = []
if self.verbosity == 0:
argv.append('--quiet')
if self.verbosity == 2:
argv.append('--verbose')
if self.verbosity == 3:
argv.append('-vv')
if self.failfast:
argv.append('--exitfirst')
if self.keepdb:
argv.append('--reuse-db')
argv.extend(test_labels)
return pytest.main(argv)
Następnie należy ustawić zmienną TEST_RUNNER w pliku settings.py.
TEST_RUNNER = 'my_project.runner.PytestTestRunner'
Teraz możemy uruchomić nasze testy w podobny sposób w jaki urucamia się je normalnie w django.
$ ./manage.py test <django args> -- <pytest args>
Uwaga
W takiej konfiguracji nie działają parametry --ds oraz --dc. Należy ustawić
te parametry w pliku pytest.ini lub wykorzystać zmienną --settings znaną z komend django.
Dodatkowe komendy¶
Uruchamiająć nasze testy mamy możliwość utworzenia dodatkowych komend.
--fail-on-template-vars dzięki której zostanie podniesiony wyjątek dla niepoprawnych zmiennych w szablonach django.
--reuse-db - ponownie wykorzystanie testowej bazy danych pomiędzy kolejnymi testami.
Testowa baza danych nie zostanie usunięta, ponowne uruchomienie testu spowoduje wykorzystanie tej bazy.
Opcja ta nie będzie przechwytywać zmian schematu między testami. Można tę opcję użyć razem z
–create-db aby ponownie utworzyć bazę danych zgodnie z nowym schematem.
--create-db - wymuszenie ponownego utworzenia testowej bazy danych, niezależnie od tego, czy istnieje, czy nie
--nomigrations - spowoduje wyłączenie migracji Django
--migrations - wymusi utworzenie bazy wraz z migracjami
Więcej szczegółów na temat konfiguracji i pracy z bazą danych można znalść pod adresem http://pytest-django.readthedocs.io/en/latest/database.html
Markery¶
pytest-django zapewnia kilka bardzo przydatnych markerów które można wykorzyastać podczas pisania testów. Wszystkie poniższe znaczniki można wykorzystać na funkcji lub klasie testujacej.
@pytest.mark.django_db(transaction=False)¶
Dzięki temu markerowi uzyskujemy dostęp do bazy danych w testach. Każdy test zostanie
przeprowadzony w ramach własnej transakcji, która zostanie wycofana po zakończeniu testu.
Ustawienie zmiennej transaction na False - domyślne zachowanie - powoduje że nasz
test zachowuje się w taki sam sposób jak wykorzystanie django.test.TestCase. Ustawienie
tej zmiennej na True powoduje zmianę zachowania na identyczną jak w django.test.TransactionTestCase.
Informacja
Aby uzyskać dostęp do bazy danych wewnątrz własnego fixture należy wykorzystać fixture db lub transactional_db.
@pytest.mark.django_db
def test_something():
obj = MyObject.objects.get(id=1)
assert obj.name == 'name'
@pytest.mark.django_db
class TestUsers:
def test_my_user(self):
me = User.objects.get(username='me')
assert me.is_superuser
class TestUsers:
pytestmark = pytest.mark.django_db
def test_my_user(self):
me = User.objects.get(username='me')
assert me.is_superuser
@pytest.mark.urls¶
Zastąpienie domyślnej konfiguracji url w django
@pytest.mark.urls('myapp.test_urls')
def test_something(client):
assert 'Success!' in client.get('/some_url_defined_in_test_urls/').content
@pytest.mark.ignore_template_errors¶
Ignorowanie niepoprawnych zmiennych w szablonie
@pytest.mark.ignore_template_errors
def test_something(client):
client('some-url-with-invalid-template-vars')
Fixtures¶
pytest-django zapewnia kilka fixture które w znaczącym stopniu ułatwiają korzystanie z wbudowanych w django dodatkowych narzędzi do testowania.
rf¶
rf jest instancją django.test.RequestFactory, która jest wykorzystywana do pisania
testów widoków bez przechodzenia przez wszystkie middleware. Dzięki temu narzędziowi możemy
przetestować konkretną zmienną lub metodę w klasie widoku.
from myapp.views import my_view
def test_details(rf):
request = rf.get('/customer/details')
response = my_view(request)
assert response.status_code == 200
client¶
client jest instancją django.test.Client. Można go wykorzystwać do pisania testów
integracyjnych jedna jest on nie polecany. Zamiast niego lepiej jest skorzystać z modułu WebTest,
który również został opisany.
def test_with_client(client):
response = client.get('/')
assert response.content == 'Foobar'
admin_client¶
admin_client jest instancją django.test.Client zalogowanego jako administrator.
def test_an_admin_view(admin_client):
response = admin_client.get('/admin/')
assert response.status_code == 200
admin_user¶
admin_user jest obiektem użytkownika (administratora) utworzonego w bazie danych.
Jego nazwa to admin a hasło password.
django_user_model¶
django_user_model jest modelem (nie instakcją) użytkownika ustawionego poprzez settings.AUTH_USER_MODEL.
def test_new_user(django_user_model):
django_user_model.objects.create(username="someone", password="something")
django_username_field¶
Jest to fixture wyodrębniający nazwę pola, używaną dla nazwy użytkownika w modelu użytkownika. Pobranie ustawienia z settings.USERNAME_FIELD.
db¶
fixture który powinien być wykorzystywany tylko w innym fixture który wymaga dostępu do bazy danych.
transactional_db¶
fixture który powinien być wykorzystywany tylko w innym fixture który wymaga dostępu do bazy danych.
live_server¶
Ten fixture uruchamia aplikację django w oddzielnym wątku. Dostęp do adresu url można uzyskać
poprzez komendę live_server.url. Ten fixture będzie przydatny w przypadku kiedy będzimy
chcieli uruchomić testy poprz wykorzystanie biblioteki selenium.
settings¶
Ten fixture zapewnia możliwość modyfikacji ustawień Django oraz automatycznie przywróci wszelkie zmiany dokonane w ustawieniach (modyfikacje, dodatki i usunięcia).
def test_with_specific_settings(settings):
settings.USE_TZ = True
assert settings.USE_TZ
django_assert_num_queries¶
Ten fixture pozwala sprawdzić oczekiwaną liczbę zapytań do bazy danych. Obecnie jest obsługiwana tylko domyślną baza danych.
def test_queries(django_assert_num_queries):
with django_assert_num_queries(3):
Item.objects.create('foo')
Item.objects.create('bar')
Item.objects.create('baz')
mailoutbox¶
Skrzynka nadawcza wiadomości e-mail, do której wysyłane są e-maile generowane przez Django.
from django.core import mail
def test_mail(mailoutbox):
mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com'])
assert len(mailoutbox) == 1
m = mailoutbox[0]
assert m.subject == 'subject'
assert m.body == 'body'
assert m.from_email == 'from@example.com'
assert list(m.to) == ['to@example.com']
Metoda setUp znana z UnitTest¶
Niestety największym dotąd nie rozwiązanym problemem jest brak możliwości tworzenia obiektów w bazie danych z wykorzystaniem metody setup_class a znanej z biblioteki UnitTest pod nazwą setUpClass.
setUpClass() ==> setup_class tearDownClass() ==> teardown_class
Natomiast aby skorzystać z znanej metody setUp() oraz skorzystać z bazy danych do tworzenia obiektów, należy nieco zmienić działanie. Oba poniższe przykłady działają dokładnie tak samo.
class AnimalTestCase(TestCase):
def setUp(self):
Animal.objects.create(name="lion", sound="roar")
Animal.objects.create(name="cat", sound="meow")
def test_animals_can_speak(self):
"""Animals that can speak are correctly identified"""
lion = Animal.objects.get(name="lion")
cat = Animal.objects.get(name="cat")
self.assertEqual(lion.speak(), 'The lion says "roar"')
self.assertEqual(cat.speak(), 'The cat says "meow"')
@pytest.mark.django_db
class AnimalTestCase:
@pytest.fixture(autouse=True)
def setup_method(self, db):
Animal.objects.create(name="lion", sound="roar")
Animal.objects.create(name="cat", sound="meow")
def test_animals_can_speak(self):
"""Animals that can speak are correctly identified"""
lion = Animal.objects.get(name="lion")
cat = Animal.objects.get(name="cat")
assert lion.speak() == 'The lion says "roar"'
assert cat.speak() == 'The cat says "meow"'
https://stackoverflow.com/questions/34089425/django-pytest-setup-method-database-issue https://github.com/pytest-dev/pytest-django/issues/297