Jak przeprowadzić testy jednostkowe z różnymi ustawieniami w Django?

117

Czy istnieje prosty mechanizm zastępowania ustawień Django w teście jednostkowym? Mam menedżera na jednym z moich modeli, który zwraca określoną liczbę najnowszych obiektów. Liczba zwracanych obiektów jest zdefiniowana przez ustawienie NUM_LATEST.

Może to spowodować niepowodzenie moich testów, gdyby ktoś zmienił ustawienie. Jak mogę zmienić ustawienia, setUp()a następnie przywrócić je tearDown()? Jeśli to nie jest możliwe, czy jest jakiś sposób, w jaki mogę małpować poprawkę metody lub kpić z ustawień?

EDYCJA: Oto mój kod menedżera:

class LatestManager(models.Manager):
    """
    Returns a specific number of the most recent public Articles as defined by 
    the NEWS_LATEST_MAX setting.
    """
    def get_query_set(self):
        num_latest = getattr(settings, 'NEWS_NUM_LATEST', 10)
        return super(LatestManager, self).get_query_set().filter(is_public=True)[:num_latest]

Menedżer używa settings.NEWS_LATEST_MAXdo dzielenia zestawu zapytań. getattr()Jest po prostu wykorzystane do zapewnienia domyślne ustawienie nie powinien istnieć.

Soviut
źródło
@Anto - czy możesz wyjaśnić dlaczego lub udzielić lepszej odpowiedzi?
użytkownik
W międzyczasie to się zmieniło; ten pierwszy zaakceptowany to ten ;)
Anto

Odpowiedzi:

164

EDYCJA: Ta odpowiedź ma zastosowanie, jeśli chcesz zmienić ustawienia dla niewielkiej liczby określonych testów.

Od wersji Django 1.4 istnieją sposoby na nadpisanie ustawień podczas testów: https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase będzie miał menedżera kontekstu self.settings, a także dekorator @override_settings, który można zastosować do metody testowej lub całej podklasy TestCase.

Te funkcje nie istniały jeszcze w Django 1.3.

Jeśli chcesz zmienić ustawienia dla wszystkich testów, zechcesz utworzyć osobny plik ustawień do testu, który będzie mógł ładować i zastępować ustawienia z głównego pliku ustawień. W innych odpowiedziach jest kilka dobrych podejść do tego zagadnienia; Widziałem udane odmiany zarówno podejścia hspandera, jak i dmitrii .

slinkp
źródło
4
Powiedziałbym, że to najlepszy sposób na zrobienie tego teraz w Django 1.4+
Michael Mior
Jak później uzyskujesz dostęp do tego ustawienia z poziomu testów? Najlepsze, jakie znalazłem, to coś w rodzaju self.settings().wrapped.MEDIA_ROOT, ale to dość okropne.
mlissner
2
Nowsze wersje Django mają do tego specjalnego menedżera kontekstu: docs.djangoproject.com/en/1.8/topics/testing/tools/ ...
Akhorus
Mój ulubiony: @modify_settings(MIDDLEWARE_CLASSES=...(dziękuję za tę odpowiedź)
guettli
44

Możesz zrobić wszystko, co chcesz z UnitTestpodklasą, w tym ustawiać i odczytywać właściwości instancji:

from django.conf import settings

class MyTest(unittest.TestCase):
   def setUp(self):
       self.old_setting = settings.NUM_LATEST
       settings.NUM_LATEST = 5 # value tested against in the TestCase

   def tearDown(self):
       settings.NUM_LATEST = self.old_setting

Ponieważ przypadki testowe django działają jednowątkowo, jestem ciekaw, co jeszcze może modyfikować wartość NUM_LATEST? Jeśli to „coś innego” zostanie wywołane przez twoją procedurę testową, to nie jestem pewien, czy jakakolwiek ilość małpich poprawek zapisze test bez unieważnienia prawdziwości samych testów.

Jarret Hardie
źródło
Twój przykład zadziałał. Otworzyło to oczy, jeśli chodzi o zakres testów jednostkowych i sposób propagacji ustawień w pliku testów w stosie wywołań.
Soviut
To nie działa z settings.TEMPLATE_LOADERS... Więc to nie jest przynajmniej ogólny sposób, ustawienia lub Django nie są przeładowywane ani nic z tą sztuczką.
Ciantic,
1
jest to dobry przykład dla wersji Django starszej niż 1.4. Dla> = 1.4 odpowiedź stackoverflow.com/a/6415129/190127 bardziej poprawna
Oduvan
Skorzystaj z docs.djangoproject.com/en/dev/topics/testing/tools/ ... Stosowanie poprawek za pomocą setUp i tearDown w ten sposób to świetny sposób na tworzenie naprawdę delikatnych testów, które są bardziej szczegółowe niż powinny. Jeśli chcesz załatać coś takiego, użyj czegoś takiego jak flexmock.
rozmyty gofr
„Ponieważ przypadki testowe django działają jednowątkowo”: co nie ma już miejsca w Django 1.9.
Wtower
22

Chociaż zastąpienie konfiguracji ustawień w czasie wykonywania może pomóc, moim zdaniem powinieneś utworzyć osobny plik do testowania. Oszczędza to dużo konfiguracji do testowania i zapewniłoby, że nigdy nie zrobisz czegoś nieodwracalnego (np. Wyczyszczenie pomostowej bazy danych).

Powiedz, że plik testowy istnieje w „my_project / test_settings.py”, dodaj

settings = 'my_project.test_settings' if 'test' in sys.argv else 'my_project.settings'

w twoim manage.py. Zapewni to, że po uruchomieniu python manage.py testbędziesz używać tylko ustawień test_settings. Jeśli używasz innego klienta testowego, takiego jak pytest, możesz równie łatwo dodać to do pytest.ini

hspandher
źródło
2
Myślę, że to dla mnie dobre rozwiązanie. Mam zbyt wiele testów i kodu, który używa pamięci podręcznej. Trudno będzie mi zmienić ustawienia jeden po drugim. Utworzę dwa pliki konfiguracyjne i określę, którego użyć. Odpowiedź MicroPyramid jest również dostępna, ale będzie niebezpiecznie, gdybym zapomniał raz dodać parametry ustawień.
ramwin
22

Możesz przejść --settingsopcję podczas uruchamiania testów

python manage.py test --settings=mysite.settings_local
MicroPyramid
źródło
przestał znajdować aplikacje, które znajdują się w settings.dev, które jest rozszerzeniem settings.base
holms
4
Myślę, że będzie to niebezpieczne, jeśli ktoś raz zapomni dodać parametry ustawień.
Ramwin
20

Aktualizacja : poniższe rozwiązanie jest potrzebne tylko w Django 1.3.x i wcześniejszych. Dla> 1.4 zobacz odpowiedź slinkpa .

Jeśli często zmieniasz ustawienia w swoich testach i używasz Pythona ≥2,5, jest to również przydatne:

from contextlib import contextmanager

class SettingDoesNotExist:
    pass

@contextmanager
def patch_settings(**kwargs):
    from django.conf import settings
    old_settings = []
    for key, new_value in kwargs.items():
        old_value = getattr(settings, key, SettingDoesNotExist)
        old_settings.append((key, old_value))
        setattr(settings, key, new_value)
    yield
    for key, old_value in old_settings:
        if old_value is SettingDoesNotExist:
            delattr(settings, key)
        else:
            setattr(settings, key, old_value)

Następnie możesz:

with patch_settings(MY_SETTING='my value', OTHER_SETTING='other value'):
    do_my_tests()
akaihola
źródło
To naprawdę fajne rozwiązanie. Z jakiegoś powodu moje ustawienia nie działały poprawnie w testach jednostkowych. Bardzo eleganckie rozwiązanie, dzięki za udostępnienie.
Tomas
Używam tego kodu, ale miałem problemy z kaskadowymi błędami testów, ponieważ ustawienia nie zostałyby przywrócone, gdyby dany test się nie powiódł. Aby rozwiązać ten problem, dodałem próbę / w końcu wokół yieldinstrukcji, z ostatnią częścią funkcji zawartą w finallybloku, aby ustawienia były zawsze przywracane.
Dustin Rasener
Zmienię odpowiedź dla potomności. Mam nadzieję, że robię to dobrze! :)
Dustin Rasener
11

@override_settings jest świetny, jeśli nie ma wielu różnic między konfiguracjami środowiska produkcyjnego i testowego.

W innym przypadku lepiej po prostu mieć inne pliki ustawień. W takim przypadku Twój projekt będzie wyglądał następująco:

your_project
    your_app
        ...
    settings
        __init__.py
        base.py
        dev.py
        test.py
        production.py
    manage.py

Musisz więc mieć większość swoich ustawień w, base.pya następnie w innych plikach musisz zaimportować wszystko stamtąd i zastąpić niektóre opcje. Oto jak test.pybędzie wyglądał Twój plik:

from .base import *

DEBUG = False

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'app_db_test'
    }
}

PASSWORD_HASHERS = (
    'django.contrib.auth.hashers.MD5PasswordHasher',
)

LOGGING = {}

Następnie albo musisz podać --settingsopcję jak w odpowiedzi @MicroPyramid, lub określić DJANGO_SETTINGS_MODULEzmienną środowiskową, a następnie możesz uruchomić testy:

export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test 
Dmitrii Michajłow
źródło
Witaj . Dmitrii, dziękuję za odpowiedź. Mam ten sam przypadek z tą odpowiedzią, ale chciałbym uzyskać więcej wskazówek na temat tego, jak aplikacja będzie wiedzieć, środowisko, w którym się znajdujemy (testowanie lub produkcja) , spójrz na mój oddział, sprawdź moje repozytorium github.com/andela/ah-backend-iroquois/tree/develop/authors , na przykład jak sobie poradzę z tą logiką?
Lutaaya Huzaifah Idris
Ponieważ używam testów nosetesty do uruchamiania testów, teraz jak to będzie działać ?, w środowisku testowym, a nie w środowisku programistycznym
Lutaaya Huzaifah Idris
3

Znalazłem to podczas próby naprawienia niektórych dokumentów ... Dla kompletności chcę wspomnieć, że jeśli zamierzasz zmodyfikować ustawienia podczas korzystania z doctestów, powinieneś to zrobić przed zaimportowaniem czegokolwiek innego ...

>>> from django.conf import settings

>>> settings.SOME_SETTING = 20

>>> # Your other imports
>>> from django.core.paginator import Paginator
>>> # etc
Jiaaro
źródło
3

O pytest użytkowników.

Największym problemem jest:

  • override_settings nie działa z pytest.
  • Podklasy Django TestCase sprawią, że to zadziała, ale nie możesz wtedy używać urządzeń pytest.

Rozwiązaniem jest użycie settingsudokumentowanego mocowania tutaj .

Przykład

def test_with_specific_settings(settings):
    settings.DEBUG = False
    settings.MIDDLEWARE = []
    ..

I na wypadek, gdybyś musiał zaktualizować wiele pól

def override_settings(settings, kwargs):
    for k, v in kwargs.items():
        setattr(settings, k, v)


new_settings = dict(
    DEBUG=True,
    INSTALLED_APPS=[],
)


def test_with_specific_settings(settings):
    override_settings(settings, new_settings)
Pithikos
źródło
3

Możesz zmienić ustawienie nawet dla pojedynczej funkcji testowej.

from django.test import TestCase, override_settings

class SomeTestCase(TestCase):

    @override_settings(SOME_SETTING="some_value")
    def test_some_function():
        

lub możesz nadpisać ustawienie dla każdej funkcji w klasie.

@override_settings(SOME_SETTING="some_value")
class SomeTestCase(TestCase):

    def test_some_function():
        
shivansh
źródło
1

Używam pytest.

Udało mi się to rozwiązać w następujący sposób:

import django    
import app.setting
import modules.that.use.setting

# do some stuff with default setting
setting.VALUE = "some value"
django.setup()
import importlib
importlib.reload(app.settings)
importlib.reload(modules.that.use.setting)
# do some stuff with settings new value
Brontes
źródło
1

Możesz zastąpić ustawienia w teście w następujący sposób:

from django.test import TestCase, override_settings

test_settings = override_settings(
    DEFAULT_FILE_STORAGE='django.core.files.storage.FileSystemStorage',
    PASSWORD_HASHERS=(
        'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
    )
)


@test_settings
class SomeTestCase(TestCase):
    """Your test cases in this class"""

A jeśli potrzebujesz tych samych ustawień w innym pliku, możesz po prostu zaimportować je bezpośrednio test_settings.

giantas
źródło
0

Jeśli masz wiele plików testowych umieszczonych w podkatalogu (pakiet Pythona), możesz nadpisać ustawienia dla wszystkich tych plików na podstawie warunku obecności ciągu „test” w sys.argv

app
  tests
    __init__.py
    test_forms.py
    test_models.py

__init__.py:

import sys
from project import settings

if 'test' in sys.argv:
    NEW_SETTINGS = {
        'setting_name': value,
        'another_setting_name': another_value
    }
    settings.__dict__.update(NEW_SETTINGS)

Nie jest to najlepsze podejście. Użyto go do zmiany brokera Selera z Redis na Memory.

Ledorub
źródło
0

Utworzyłem nowy plik settings_test.py, który importowałby wszystko z pliku settings.py i modyfikował wszystko, co było inne do celów testowych. W moim przypadku podczas testowania chciałem użyć innego zasobnika do przechowywania w chmurze. wprowadź opis obrazu tutaj

settings_test.py:

from project1.settings import *
import os

CLOUD_STORAGE_BUCKET = 'bucket_name_for_testing'

manage.py:

def main():

    # use seperate settings.py for tests
    if 'test' in sys.argv:
        print('using settings_test.py')
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project1.settings_test')
    else:
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project1.settings')

    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
Aseem
źródło