Ustawienia¶
Testując aplikację lokalnie bardzo ważne jest aby testy uruchamiały się bardzo szybko, sprawia to, że nasza uwaga jest poświęcona cały czas na pisaniu dobrego kodu. Aby przyspieszyć wykonywanie testów w Django istnieje kilka dobrych praktyk które spowodują że testy będą działać odczuwalnie szybciej.
Zmień hashowanie hasła¶
Jest to najskuteczniejsze ustawienie, które można wykorzystać do poprawy szybkości testów.
Może się to wydawać śmieszne, ale hashowanie haseł w Django jest bardzo mocne, dlatego
korzysta on z kilku „hasherów”, oznacza to jednak, że haszowanie jest bardzo powolne.
Najszybszym hasherem jest MD5PasswordHasher, dlatego warto go użyć podczas testowania
aplikacji:
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
)
Użyj SQLite w pamięci¶
Obecnie najszybszą bazą danych z której korzysta Django jest SQLite. Testujemy własną implementację kodu, własne API i jeśli nie używamy surowych zapytań SQL, bazowy mechanizm przechowywania danych nie powinien pokazywać różnic!
Warto więc zmienić go na silnik SQLite:
# test_settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
#if manage.py test was called, use test settings
if 'test' in sys.argv:
try:
from test_settings import *
except ImportError:
pass
Uwaga
Jeśli wykorzystujemy continuous integration nie powinniśmy podmieniać ustawień bazy danych. Takie środowisko powinno być najbardziej zbliżone do środowiska produkcyjnego.
Również jeśli wykonujemy testy integracyjne (nie powinny one być połączone z testami jednostkowymi w tym samym pliku) nie powinniśmy również zmieniać ustawień bazy danych.
Informacja
Jeśli wykorzystujemy specyficzne rozwiązania z silnika bazy danych z której korzystamy, możemy tagować nasze testy markerami, zapewni nam to możliwość uruchomienia testów specyficznych dla danej bazy danych oraz do szybkie testowanie zapytań napisanych w Django ORM.
import pytest
@pytest.mark.postgres
class TestSpecificForPostgreSQL(TestCase):
def test_save_json_field_from_api(self):
...
Warto w ustawieniach dodać brak możliwości podmiany ustawień bazy danych podczas wykonywania testów.
# test_settings.py
if not 'not postgres' in sys.argv and 'test' in sys.argv:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
Uruchomienie testów jest bardzo proste. Wystarczy w testach podać atrybut uruchamiający
wszystkie testy poza testami z markerem postgres.
$ pytest -v -m "not postgres"
Innym sposobem na rozwiązanie tego problemu jest napisanie nakładek na specyficzne pola dla danego silnika bazy danych. Niestety nie miałem z tym większej styczności dlatego przekierowuję do jednego z artykułów.
https://www.aychedee.com/2014/03/13/json-field-type-for-django/
Usuń niepotrzebne middleware¶
Im więcej klas middleware, tym więcej czasu będzie potrzebne na wygenerowanie odpowiedzi (ponieważ wszystkie warstwy pośredniczące muszą być wykonywane sekwencyjnie przed zwróceniem ostatecznej odpowiedzi HTTP). Warto więc uruchomić tylko te warstwy których tak naprawdę potrzebujesz!
Szczególnie jeden middleware jest bardzo wolny:
django.middleware.locale.LocaleMiddleware
Możemy założyć, że wszystkie middleware z Django działają poprawnie, dlatego podczas testowania możemy je usunąć, aby uniknąć wszystkich narzutów podczas wysyłania żądań.
MIDDLEWARE_CLASSES = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
Usuń niepotrzebne aplikacje¶
Istnieje kilka aplikacji, które można usunąć podczas testowania, np. django-debug-toolbar
czy django_extension spróbuj usunąć wszystkie nieużywane/niepotrzebne aplikacje podczas
wykonywania testów.
Wyłącz debugowanie¶
Ustawienie parametru DEBUG=False podczas uruchamiania testów zmniejsza obciążenie
związane z debugowaniem, dzięki czemu poprawia się szybkość wykonywania testów.
DEBUG = False
Wyłącz informacje o logach¶
Jest to znacząca modyfikacja tylko wtedy, gdy mamy ogromną ilość logowań i/lub dodatkowej logiki związanej z logami (np. inspekcje obiektów, ciężkie manipulacje ciągami itd.). Logowanie również jest niepotrzebne podczas wykonywania testów, dlatego nie ma potrzeby dodawania dodatkowego narzutu pliku I/O do pakietu testowego.
import logging
logging.disable(logging.CRITICAL)
Użyj szybszego zaplecza e-mail¶
Domyślnie Django używa django.core.mail.backends.locmem.EmailBackend, który jest
backendem przeznaczonym do testowania w pamięci, jednak czasem mogą z nim wystąpić problemy
z powodu sprawdzanie nagłówków. Warto więc skorzystąć z alternatywnego backendu mailowego.
EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
Używaj Celery uruchamianego w pamięci¶
Jeśli wykorzystujesz Celery w swoich projektach warto zmienić ustawienia do testowania:
CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
BROKER_BACKEND = 'memory'
Mock, mock, mock!¶
Wykorzystując Mock możesz znacznie skrócić czas testowania swoich aplikacji.
Obiekty Mock można używać podczas każdych testów, najeży jednak pamiętać aby nie tworzyć
mocków do bazy danych jeśli nie posiadamy testów integracyjnych. Więcej szczegułów
na temat tworzenia Mock znajdziesz w module pytest-mock.
Dodatkowe opcje¶
Domyślne ustawienie lokalizacj dla Faker¶
import pytest
from requests_mock import MockerCore
from factory.faker import Faker
from faker import config
Faker._DEFAULT_LOCALE = 'pl_PL'
config.DEFAULT_LOCALE = 'pl_PL'
Funkcja testująca metody widoków¶
def setup_view(view, request, *args, **kwargs):
"""
Mimic as_view() returned callable, but returns view instance.
args and kwargs are the same you would pass to ``reverse()``
Example:
name = 'django'
request = RequestFactory().get('/fake-path')
view = HelloView(template_name='hello.html')
view = setup_view(view, request, name=name)
Example test ugly dispatch():
response = view.dispatch(view.request, *view.args, **view.kwargs)
"""
view.request = request
view.args = args
view.kwargs = kwargs
return view
Funkcja testująca metody widoków API¶
def api_setup_view(view, request, action=None, *args, **kwargs):
"""
request = HttpRequest()
view = views.ProfileInfoView()
view = api_setup_view(view, request, 'list')
assert view.get_serializer_class() == view.serializer_class
"""
view.request = request
view.action = action
view.args = args
view.kwargs = kwargs
return view
Klasa APIRequestFactory jako fixture¶
@pytest.fixture()
def api_rf():
"""
APIRequestFactory instance
"""
skip_if_no_django()
from rest_framework.test import APIRequestFactory
return APIRequestFactory()
Biblioteka requests_mock jako fixture¶
import pytest
from requests_mock import MockerCore
# --------------------------------------------------------------------
# dodatek pozwalający w łatwy sposób robić mock dla biblioteki request
# --------------------------------------------------------------------
@pytest.yield_fixture(scope="session")
def requests_mock():
"""
def test_get_tags(self, requests_mock):
requests_mock.get(settings.MY_SERVICE + 'tag/', json=response)
cron = ImportTriviaCromJob()
assert list(cron.get_tags(name)) == result
"""
mock = MockerCore()
mock.start()
yield mock
mock.stop()
Fixture dla DjangoLiveServer w kontenerze Docker¶
import pytest
from pytest_django.lazy_django import skip_if_no_django
from pytest_django.live_server_helper import LiveServer
# ------------------------------------------------------------------------
# dodatek pozwalający na uruchomienie DjangoLiveServer w kontenerze Docker
# ------------------------------------------------------------------------
@pytest.fixture(scope='session')
def live_server(request):
server = DockerLiveServer()
request.addfinalizer(server.stop)
return server
class DockerLiveServer(LiveServer):
def __init__(self):
import socket
self.addr = socket.gethostbyname(socket.gethostname())
import django
from django.db import connections
from django.test.testcases import LiveServerThread
from django.test.utils import modify_settings
connections_override = {}
for conn in connections.all():
# If using in-memory sqlite databases, pass the connections to
# the server thread.
if conn.vendor == 'sqlite' and conn.is_in_memory_db(conn.settings_dict['NAME']):
# Explicitly enable thread-shareability for this connection
conn.allow_thread_sharing = True
connections_override[conn.alias] = conn
liveserver_kwargs = {'connections_override': connections_override}
from django.conf import settings
if 'django.contrib.staticfiles' in settings.INSTALLED_APPS:
from django.contrib.staticfiles.handlers import StaticFilesHandler
liveserver_kwargs['static_handler'] = StaticFilesHandler
else:
from django.test.testcases import _StaticFilesHandler
liveserver_kwargs['static_handler'] = _StaticFilesHandler
if django.VERSION < (1, 11):
host, possible_ports = self.addr, [8081]
self.thread = LiveServerThread(host, possible_ports, **liveserver_kwargs)
else:
host = self.addr
self.thread = LiveServerThread(host, **liveserver_kwargs)
self._live_server_modified_settings = modify_settings(
ALLOWED_HOSTS={'append': host}
)
self._live_server_modified_settings.enable()
self.thread.daemon = True
self.thread.start()
self.thread.is_ready.wait()
if self.thread.error:
raise self.thread.error
@property
def url(self):
if self.thread.host == self.addr:
return 'http://%s:%s' % ('localhost', self.thread.port)
return 'http://%s:%s' % (self.thread.host, self.thread.port)