├── page ├── modules │ ├── jupyter.rst │ ├── django_extensions.rst │ └── django_debug_toolbar.rst ├── django │ ├── celery.rst │ ├── forms.rst │ ├── admin.rst │ ├── urls.rst │ ├── models.rst │ ├── views.rst │ ├── index.rst │ ├── django_rest_framework.rst │ ├── django_webtest.rst │ ├── pytest_django.rst │ └── settings.rst ├── pytest │ ├── pytest_selenium.rst │ ├── benchmark.jpg │ ├── pytest-sugar.gif │ ├── benchmark-compare.jpg │ ├── pytest_sugar.rst │ ├── pytest_pudb.rst │ ├── pytest_cov.rst │ ├── pytest_xdist.rst │ ├── pytest_faker.rst │ ├── pytest_benchmark.rst │ ├── pytest.rst │ ├── pytest_mock.rst │ └── pytest_factoryboy.rst ├── extra │ ├── locust.rst │ └── pylama.rst └── bibliography.rst ├── example.rst ├── index.rst └── conf.py /page/modules/jupyter.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Jupyter 3 | ======= 4 | 5 | Przygotować opis -------------------------------------------------------------------------------- /page/django/celery.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Testowanie Celery 3 | ================= 4 | 5 | Opis -------------------------------------------------------------------------------- /page/pytest/pytest_selenium.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Pytest Selenium 3 | =============== 4 | 5 | Przygotować opis -------------------------------------------------------------------------------- /page/modules/django_extensions.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Django Extensions 3 | ================= 4 | 5 | Przygotować opis -------------------------------------------------------------------------------- /page/pytest/benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witold-gren/pytest-django-testing/HEAD/page/pytest/benchmark.jpg -------------------------------------------------------------------------------- /page/pytest/pytest-sugar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witold-gren/pytest-django-testing/HEAD/page/pytest/pytest-sugar.gif -------------------------------------------------------------------------------- /page/modules/django_debug_toolbar.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Django Debug Toolbar 3 | ==================== 4 | 5 | Przygotować opis 6 | -------------------------------------------------------------------------------- /page/pytest/benchmark-compare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witold-gren/pytest-django-testing/HEAD/page/pytest/benchmark-compare.jpg -------------------------------------------------------------------------------- /page/extra/locust.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Locust - testy wydajnościowe 3 | ============================ 4 | 5 | Przygotować opis 6 | -------------------------------------------------------------------------------- /page/django/forms.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Testowanie formularzy 3 | ===================== 4 | 5 | Opis... 6 | 7 | Podstawy 8 | -------- 9 | 10 | Opis... -------------------------------------------------------------------------------- /page/django/admin.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Testowanie panelu administracyjnego 3 | =================================== 4 | 5 | Opis... 6 | 7 | Podstawy 8 | -------- 9 | 10 | Opis... -------------------------------------------------------------------------------- /page/pytest/pytest_sugar.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Pytest Sugar 3 | ============ 4 | 5 | 6 | ``pytest-sugar`` jest to dodatek do ``pytest``, który w bardzo przystępny sposób pokazuje 7 | awarie i błędy podczas wykonywania testów oraz podmienia pasek postępu. 8 | 9 | 10 | .. image:: pytest-sugar.gif 11 | 12 | 13 | Instalacja 14 | ---------- 15 | 16 | .. code-block:: bash 17 | 18 | $ pip install pytest-sugar 19 | -------------------------------------------------------------------------------- /example.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Building your own shop 3 | ====================== 4 | 5 | 6 | The sandbox site 7 | ---------------- 8 | 9 | asdfasdfasdf 10 | 11 | 12 | Browse the external sandbox site 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 15 | .. warning:: 16 | 17 | asdfasdf 18 | 19 | 20 | Run the sandbox locally 21 | ~~~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | 24 | .. code-block:: bash 25 | 26 | $ git clone https://github.com/django-oscar/django-oscar.git 27 | 28 | 29 | 30 | .. tip:: 31 | 32 | You can always review the set-up of the 33 | 34 | 35 | .. attention:: 36 | 37 | Please ensure that ``pillow`` 38 | 39 | -------------------------------------------------------------------------------- /page/django/urls.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Testowanie linków 3 | ================= 4 | 5 | W taki sposób możemy sprawdzić czy wywołanie konkretnego adresu url spowoduje 6 | uruchomienie wskazanej przez nas klasy lub funkcji widoku. 7 | 8 | Django Class Base View 9 | ^^^^^^^^^^^^^^^^^^^^^^ 10 | 11 | .. code-block:: python 12 | 13 | from users.views import ReferralsView 14 | 15 | found = resolve(reverse('referrals')) 16 | assert found.func.view_class == ReferralsView 17 | 18 | 19 | Django Function View 20 | ^^^^^^^^^^^^^^^^^^^^ 21 | 22 | .. code-block:: python 23 | 24 | from chat.views import get_chats 25 | 26 | found = resolve(reverse('referrals')) 27 | assert found2.func == get_chats 28 | -------------------------------------------------------------------------------- /page/pytest/pytest_pudb.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Pytest Pudb 3 | =========== 4 | 5 | ``pytest-pudb`` jest to wtyczka do ``pytest``, która ułatwia wruchomienie debagera ``pudb`` 6 | podczas testowania. Podczas uruchamiania debugera należy wyłączyć przechwytywanie konsoli 7 | wykorzystaując flagę ``-s`` lub ``--capture=no``. Więcej informacji można uzyskać korzystając 8 | z dokumentacji pudb_. 9 | 10 | 11 | .. _pudb: https://documen.tician.de/pudb/index.html 12 | 13 | 14 | .. hint:: 15 | 16 | Korzystając z ``docker-compose`` należy uruchomić go w odpowiednim trybie. Dzięki temu 17 | konsola przekaże nam okno do debugowania: ``docker-compose run --service-ports CONTAINER_NAME``. 18 | 19 | 20 | Instalacja 21 | ---------- 22 | 23 | .. code-block:: bash 24 | 25 | $ pip install pytest-pudb 26 | 27 | 28 | Uruchomienie 29 | ------------ 30 | 31 | .. code-block:: bash 32 | 33 | $ pytest --pudb -s 34 | $ pytest --pdbcls pudb.debugger:Debugger --pdb -s 35 | 36 | 37 | .. code-block:: python 38 | 39 | def test_set_trace_integration(): 40 | import pudb; pudb.set_trace() 41 | assert 1 == 2 42 | 43 | def test_pudb_b_integration(): 44 | import pudb.b 45 | # traceback is set up here 46 | assert 1 == 2 -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | Efektywne pisanie testów w języku python 2 | ======================================== 3 | 4 | Niniejsza dokumentacja został przygotowana aby przedstawić najbardziej efektywne techniki 5 | pisania testów w języku Python oraz w frameworku Django. Istnieje bardzo dużo artykułów 6 | i samouczków w internecie, ale żaden z nich nie zbiera całej wiedzy w jednym dokumencie. 7 | Są one skupione na pojedynczych elementach testowania, lub na specyficznych problemach. 8 | 9 | Większość samouczków wykorzystuje domyślną bibliotekę ``UnitTest``. Brakuje jednak 10 | dokumentu opisującego wspaniałe narzędzie jakim jest ``PyTest`` oraz opisu pluginów które 11 | można wykorzystać podczas pisania testów, a które w znacznym stopniu ułatwiają pracę 12 | programisty. 13 | 14 | Niniejszy dokument został podzielony na trzy częsci. Pierwsza opisuje zastosowanie 15 | modułu ``PyTest`` wraz z kilkoma bardzo przydatnymi dodatkami. Druga część opisuje 16 | w jaki sposób efektywnie pisać testy dla frameworka Django. Trzecia część skupia się na 17 | dodatkowych narzędziach, które można wykorzystać w pracy z Django. 18 | 19 | 20 | Aby zainstalować wszystkie opisywane paczki, należy uruchomić poniższą komendę: 21 | 22 | .. code-block:: bash 23 | 24 | $ pip install pytest pytest-xdist pytest-pudb pytest-cov pytest-django pytest-factoryboy 25 | pytest-faker pytest-selenium pytest-mock pytest-benchmark jupyter django-extensions 26 | django-debug-toolbar django-webtest pylama 27 | 28 | 29 | .. toctree:: 30 | :maxdepth: 1 31 | :caption: Poznaj PyTest: 32 | 33 | page/pytest/pytest 34 | page/pytest/pytest_sugar 35 | page/pytest/pytest_xdist 36 | page/pytest/pytest_pudb 37 | page/pytest/pytest_cov 38 | page/pytest/pytest_factoryboy 39 | page/pytest/pytest_faker 40 | page/pytest/pytest_mock 41 | page/pytest/pytest_benchmark 42 | page/pytest/pytest_selenium 43 | page/extra/pylama 44 | page/extra/locust 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | :caption: Testowanie aplikacji Django: 49 | 50 | page/django/pytest_django 51 | page/django/django_webtest 52 | page/django/index 53 | 54 | 55 | .. toctree:: 56 | :maxdepth: 1 57 | :caption: Przydatne moduły do pracy z Django: 58 | 59 | page/modules/jupyter 60 | page/modules/django_extensions 61 | page/modules/django_debug_toolbar 62 | 63 | 64 | Indices and tables 65 | ================== 66 | 67 | * :ref:`genindex` 68 | * :ref:`search` 69 | * :ref:`bibliography` 70 | 71 | 72 | .. _pytest: https://docs.pytest.org 73 | .. _pytest-django: https://pytest-django.readthedocs.io/en/latest/ 74 | .. _pytest-factoryboy: http://pytest-factoryboy.readthedocs.io/en/latest/ 75 | .. _pytest-mock: https://github.com/pytest-dev/pytest-mock/ 76 | .. _pytest-faker: http://pytest-faker.readthedocs.io/en/latest/ 77 | .. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist 78 | .. _pytest-pudb: https://github.com/wronglink/pytest-pudb 79 | .. _pytest-benchmark: https://pytest-benchmark.readthedocs.io/en/latest/ 80 | .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ 81 | .. _pytest-selenium: http://pytest-selenium.readthedocs.io/en/latest/index.html 82 | .. _jupyter: http://jupyter.readthedocs.io/en/latest/index.html 83 | .. _django-extensions: https://django-extensions.readthedocs.io/en/latest/ 84 | .. _django-debug-toolbar: https://django-debug-toolbar.readthedocs.io/en/stable/ 85 | .. _pylama: https://github.com/klen/pylama 86 | -------------------------------------------------------------------------------- /page/pytest/pytest_cov.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Pytest Coverage 3 | =============== 4 | 5 | Coverage jest pluginem sprawdzającym pokrycie kodu testami wyrażone w procentach. 6 | pytest-cov_ jest dodatkiem do ``pytest`` ułatwiający pracę z `coverage`_ - oryginalnym 7 | pluginem napisanym dla języka python. Dodatek ten oprócz wszystkich możliwości standardowej 8 | biblioteki, pozwala dodatkowo uruchomić sprawdzenie pokrycia kodu podczas testowana 9 | (dodatkowe parametry do pytest), pozwala wraz z pluginem `pytest-xdist` uruchimić 10 | testowanie na kilku wątkach. 11 | 12 | 13 | Instalacja 14 | ---------- 15 | 16 | .. code-block:: bash 17 | 18 | $ pip install pytest-cov 19 | 20 | 21 | Konfiguracja 22 | ------------ 23 | 24 | Możemy skonfigurować oryginalny plik ``.coveragerc`` a później wskazać go podczas 25 | uruchomienia konfiguracji. Więcej informacji o konfiguracji znajdziemy w `dokumentacji coverage`_ 26 | 27 | 28 | .. code-block:: bash 29 | 30 | [run] 31 | omit = tests/* 32 | 33 | 34 | Uruchomienie pytest wraz ze wskazaniem pliku do kofiguracji dla ``coverage``. 35 | 36 | .. code-block:: bash 37 | 38 | pytest --cov-config .coveragerc --cov=myproj myproj/tests/ 39 | 40 | 41 | Generowanie raportów 42 | -------------------- 43 | 44 | Posiadamy kilka opcji podczas generowania raportów: 45 | 46 | * term - uruchomienie raportu w terminalu 47 | * term-missing - uruchomienie raportu w terminalu wraz z informacją jakie linie kodu zostały przetestowane 48 | * html - generowanie raportu html 49 | * xml - generowanie raportu xml 50 | * annotate - tworzy nam kopię plików i oznacza w nich co zostało przetestowane 51 | * :skip-covered - opcja z pominięciem kodu który jest przetestowany w 100% 52 | 53 | 54 | ``skip-covered`` można łączyć wraz z innymi generatorami w celu pominięcia raportu który jest 55 | pokryty w 100%. 56 | 57 | 58 | .. code-block:: bash 59 | 60 | $ pytest --cov-report term --cov=myproj tests/ 61 | 62 | -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- 63 | Name Stmts Miss Cover 64 | ---------------------------------------- 65 | myproj/__init__ 2 0 100% 66 | myproj/myproj 257 13 94% 67 | myproj/feature4286 94 7 92% 68 | ---------------------------------------- 69 | TOTAL 353 20 94% 70 | 71 | 72 | .. code-block:: bash 73 | 74 | $ pytest --cov-report term:skip-covered --cov=myproj tests/ 75 | 76 | 77 | .. code-block:: bash 78 | 79 | $ pytest --cov-report html 80 | --cov-report xml 81 | --cov-report annotate 82 | --cov=myproj tests/ 83 | 84 | 85 | Debagowanie i IDE 86 | ----------------- 87 | 88 | Podczas gdy mamy zainstalowany dodatek używa on ``sys.settrace`` jako dostęp do kontekstu. 89 | Dlatego wykorzystując jakiekolwiek IDE należy wyłączyć plugin aby nie powodował problemów 90 | podczas wykorzystywnia np. ``break point``. 91 | 92 | .. code-block:: bash 93 | 94 | $ pytest –no-cov 95 | 96 | 97 | Markery 98 | ------- 99 | 100 | Jest to marker pozwalający nam na całkowite wyłączenie konkretnego testu podczas tworzenia 101 | raportu kodu pokrytego testami. 102 | 103 | .. code-block:: python 104 | 105 | @pytest.marker.no_cover 106 | def test_foobar(): 107 | ... 108 | 109 | 110 | .. _`coverage`: https://coverage.readthedocs.io/en/latest/ 111 | .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ 112 | .. _`dokumentacji coverage`: https://coverage.readthedocs.io/en/latest/config.html -------------------------------------------------------------------------------- /page/bibliography.rst: -------------------------------------------------------------------------------- 1 | .. _bibliography: 2 | 3 | ============ 4 | Bibliografia 5 | ============ 6 | 7 | YouTube PL: 8 | ----------- 9 | 10 | * `PyCon PL 2013 - Zaawansowane testowanie jednostkowe aplikacji Django`_ 11 | * `PyCon PL 2014 - "pytest + tox = tandem idealny?"`_ 12 | 13 | .. _`PyCon PL 2013 - Zaawansowane testowanie jednostkowe aplikacji Django`: https://www.youtube.com/watch?v=K6QpwsLRn1g&t=86s 14 | .. _`PyCon PL 2014 - "pytest + tox = tandem idealny?"`: https://www.youtube.com/watch?v=pXo4H8XTdGQ&t=204s 15 | 16 | 17 | YouTube EN: 18 | ----------- 19 | * `Testing and Django`_ 20 | * `pytest - rapid and simple testing with Python`_ 21 | * `Advanced Uses of py.test Fixtures`_ 22 | * `Fast Test, Slow Test`_ 23 | 24 | .. _`pytest - rapid and simple testing with Python`: https://www.youtube.com/watch?v=9LVqBQcFmyw&t=1936s 25 | .. _`Advanced Uses of py.test Fixtures`: https://www.youtube.com/watch?v=IBC_dxr-4ps&t=753s 26 | .. _`Testing and Django`: https://www.youtube.com/watch?v=ickNQcNXiS4 27 | .. _`Fast Test, Slow Test`: https://www.youtube.com/watch?v=RAxiiRPHS9k&t=748s 28 | 29 | 30 | Strony internetowe: 31 | ------------------- 32 | 33 | * `Testing Models with Django using Pytest and Factory Boy`_ 34 | * `Test Driven Django Tutorial`_ 35 | * `Test Driven Django Development`_ 36 | * `Effective TDD tricks to speed up django tests up to 10x faster`_ 37 | * `Django Factory Audit`_ 38 | * `Tests in django`_ 39 | * `A JSON field type for Django`_ 40 | * `Testing Web Apps with Python, Selenium, Django`_ 41 | * `Introductions to Python Testing Frameworks`_ 42 | * `ADVANCED PY.TEST FIXTURES`_ 43 | 44 | .. _`TESTS IN DJANGO`: http://kartowicz.com/dryobates/2015-10/tests_in_django/ 45 | .. _`Testing Models with Django using Pytest and Factory Boy`: https://medium.com/@dwernychukjosh/testing-models-with-django-using-pytest-and-factory-boy-a2985adce7b3 46 | .. _`A JSON field type for Django`: https://www.aychedee.com/2014/03/13/json-field-type-for-django/ 47 | .. _`Introductions to Python Testing Frameworks`: http://pythontesting.net/start-here/ 48 | .. _`Testing Web Apps with Python, Selenium, Django`: http://testandcode.com/9?t=0 49 | .. _`Test Driven Django Tutorial`: https://github.com/hjwp/Test-Driven-Django-Tutorial 50 | .. _`Test Driven Django Development`: http://test-driven-django-development.readthedocs.io 51 | .. _`Effective TDD tricks to speed up django tests up to 10x faster`: http://www.daveoncode.com/2013/09/23/effective-tdd-tricks-to-speed-up-django-tests-up-to-10x-faster/ 52 | .. _`Django Factory Audit`: http://jamescooke.info/django-factory-audit.html 53 | .. _`ADVANCED PY.TEST FIXTURES`: http://devork.be/talks/advanced-fixtures/advfix.html 54 | 55 | 56 | Python Mock: 57 | ------------ 58 | 59 | * `Python unit testing with Pytest and Mock`_ 60 | * `Python Mock Cookbook`_ 61 | * `Python Mock Gotchas`_ 62 | * `Python Mocks - a gentle introduction`_ 63 | 64 | .. _`Python unit testing with Pytest and Mock`: https://medium.com/@bfortuner/python-unit-testing-with-pytest-and-mock-197499c4623c 65 | .. _`Python Mock Cookbook`: https://chase-seibert.github.io/blog/2015/06/25/python-mocking-cookbook.html 66 | .. _`Python Mock Gotchas`: http://alexmarandon.com/articles/python_mock_gotchas/ 67 | .. _`Python Mocks - a gentle introduction`: http://blog.thedigitalcatonline.com/blog/2016/03/06/python-mocks-a-gentle-introduction-part-1/ 68 | 69 | Książki: 70 | -------- 71 | 72 | * `TDD. Sztuka tworzenia dobrego kodu`_ 73 | * `TDD w praktyce. Niezawodny kod w języku Python`_ 74 | 75 | .. _`TDD. Sztuka tworzenia dobrego kodu`: https://helion.pl/ksiazki/tdd-sztuka-tworzenia-dobrego-kodu-kent-beck,tddszt.htm 76 | .. _`TDD w praktyce. Niezawodny kod w języku Python`: https://helion.pl/ksiazki/tdd-w-praktyce-niezawodny-kod-w-jezyku-python-harry-j-w-percival,tddwpr.htm -------------------------------------------------------------------------------- /page/extra/pylama.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Pylama - audyt kodu 3 | =================== 4 | 5 | Pylama jest narzędzie do audytu kodu dla pythona. Moduł ten integruje i wykorzystuje 6 | kilka zewnętrznych modułów: 7 | 8 | * `pycodestyle`_ (sprawdzanie poprawności kodu pod kątem PEP8) 9 | * `pydocstyle`_ (sprawdzenie poprawności docstring którego opis znajduje się w PEP257) 10 | * `pyflakes`_ (program, który sprawdza pliki źródłowe Pythona pod kątem błędów) 11 | * `mccabe`_ (mikro-narzędzie do sprawdzania złożoności cyklomatycznej kodu) 12 | * `pylint`_ (narzędzie do weryfikacji kodu) 13 | * `radon`_ (narzędzie do obliczania różnych metryk kodu) 14 | 15 | .. _`pycodestyle`: http://pycodestyle.pycqa.org/en/latest/ 16 | .. _`pydocstyle`: http://www.pydocstyle.org/en/2.1.1/ 17 | .. _`pyflakes`: https://github.com/PyCQA/pyflakes 18 | .. _`mccabe`: https://github.com/pycqa/mccabe 19 | .. _`pylint`: https://pylint.org/ 20 | .. _`radon`: http://radon.readthedocs.io/en/latest/ 21 | 22 | 23 | Instalacja 24 | ---------- 25 | 26 | .. code-block:: bash 27 | 28 | $ pip install pylama 29 | 30 | 31 | Ustawienia 32 | ---------- 33 | 34 | Ustawienia tego modułu można dokonać w kilku plikach: 35 | 36 | * pylama.ini 37 | * setup.cfg 38 | * tox.ini 39 | * pytest.ini 40 | 41 | Istnieje kilka konfiguracji. Pierwszą z nich jest sekcja "`pylama`" konfigurująca globalne opcje. 42 | 43 | .. code-block:: bash 44 | 45 | [pylama] 46 | format = pylint 47 | skip = */.tox/*,*/.env/* 48 | linters = pylint,mccabe 49 | ignore = F0401,C0111,E731 50 | 51 | Można również ustawić opcje specjalnego sprawdzania kodu dla poszczególnych konfiguracjami narzędzi. 52 | 53 | .. code-block:: bash 54 | 55 | [pylama:pyflakes] 56 | builtins = _ 57 | 58 | [pylama:pycodestyle] 59 | max_line_length = 100 60 | 61 | [pylama:pylint] 62 | max_line_length = 100 63 | disable = R 64 | 65 | Ostanią możliwością jest ustawienie opcji dla pliku (lub grupy plików) w sekcjach. 66 | Opcje te mają wyższy priorytet niż sekcja "pylama". 67 | 68 | .. code-block:: bash 69 | 70 | [pylama:*/pylama/main.py] 71 | ignore = C901,R0914,W0212 72 | select = R 73 | 74 | [pylama:*/tests.py] 75 | ignore = C0110 76 | 77 | [pylama:*/setup.py] 78 | skip = 1 79 | 80 | 81 | Wykorzystanie 82 | ------------- 83 | 84 | Pylama posiada wsparcie dla ``pytest``. Pakiet automatycznie rejestruje się jako dodatek 85 | do pytest podczas instalacji. 86 | 87 | .. code-block:: bash 88 | 89 | $ pytest --pylama ... 90 | 91 | 92 | Więcej szczegółów konfiguracyjnych można znaleźć na stronie https://pylama.readthedocs.io/en/latest/ 93 | 94 | 95 | Złożoność cyklomatyczna McCabe'a 96 | -------------------------------- 97 | 98 | Złożoność cyklomatyczna (CC), mimo swojej długiej historii – została zdefiniowana w 1976 99 | roku z myślą o programowaniu strukturalnym – jest nadal podstawową miarą złożoności 100 | dowolnego fragmentu kodu. 101 | 102 | ================== =============== 103 | wartość CC Interpretacja 104 | ================== =============== 105 | 1 - 10 prosta metoda 106 | 11 - 20 metoda złożona 107 | 21 - 50 metoda bardzo złożona 108 | > 50 testowanie niemal niemożliwe 109 | ================== =============== 110 | 111 | 112 | Możliwości modułu Radom 113 | ----------------------- 114 | 115 | * obliczenie złożoność cyklomatycznej 116 | * całkowita liczba linii kodu (LOC) 117 | * liczba logicznych linii kodu (LLOC) 118 | * liczba linii źródłowych kodu (SLOC) 119 | * liczba linii komentarza 120 | * liczba linii reprezentujących wieloliniowe ciągi 121 | * liczba pustych linii 122 | * złożoność Halsteada (trudność, poziom programu, wysiłek, czas, szacunkowa liczba błędów itd.) 123 | -------------------------------------------------------------------------------- /page/pytest/pytest_xdist.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Pytest Xdist 3 | ============ 4 | 5 | Wtyczka pytest-xdist rozszerza pytest o kilka unikalnych trybów wykonywania testów: 6 | 7 | * ``testowanie ciągłe`` (looponfail): uruchamiaj swoje testy w podprocesie. Uruchamiając 8 | ``pytest`` w tym trybie, najpierw są uruchamiane wszystkie testy. Po ich przejściu ``pytest`` 9 | oczekuje na zmiany w plikach. Po ich dokonaniu są uruchamiane tylko te testy które są 10 | zależne od zmienionych plików. Jeśli wszystko będzie poprawne, ponownie są uruchamiane 11 | wszystkie testy, jeśli natomiast będą błędy, ``pytest`` testuje tylko te wybrane testy 12 | do czasu aż nie przejdą poprawnie. 13 | 14 | * ``wieloprocesowe testowanie`` (`multiprocess load-balancing`): jeśli posiadasz wiele 15 | procesorów lub hostów, możesz ich użyć do złożonego testowawnia. Pozwala to przyspieszyć 16 | wykonywanie testów oraz pozwala skorzystać ze zasobów zdalnych maszyn. 17 | 18 | * ``pokrycie wieloplatformowe`` (`multi-platform coverage`): pozwala określić różne 19 | interpretery języka python lub różne platformy oraz pozwala uruchamiać testy 20 | równolegle na wszystkich z nich. 21 | 22 | 23 | Instalacja 24 | ---------- 25 | 26 | .. code-block:: python 27 | 28 | pip install pytest-xdist 29 | 30 | 31 | Uruchamianie 32 | ------------ 33 | 34 | Wykorzystanie wielu procesorów do uruchamiania testów 35 | 36 | .. code-block:: bash 37 | 38 | $ pytest -n NUM 39 | 40 | 41 | Uruchamianie testów w subprocesach pythona 42 | 43 | .. code-block:: bash 44 | 45 | $ pytest -d --tx popen//python=python2.7 46 | $ pytest -d --tx 3*popen//python=python2.7 47 | 48 | 49 | Uruchomienie testów w trybie `looponfailing` 50 | 51 | Tryb ten jest wykorzystywany podczas refaktoryzacji kodu. Zakładając, że występują 52 | testy które nie przechodzą, ``pytest`` będzie oczekiwać na zmiany w plikach które, 53 | zmieniają test a natępnie ponownie uruchomią zestaw testów. Zmiany w plikach są wykrywane 54 | przez przeglądanie katalogu głównego root komputerów oraz ich zawartości (rekursywnie). 55 | 56 | Niestety nie ma możliwości wykluczenia katalogów, sprawdzamy korzeń root oraz wszystkie 57 | fildery i pliki zagnieżdżone. Jest to problemem kiedy struktura projektu jest zbudowana 58 | w niepoprawny sposób. 59 | 60 | .. code-block:: bash 61 | 62 | $ pytest -f ... 63 | 64 | 65 | Wykorzystanie kont ssh aby wysłać i uruchomić testy 66 | 67 | .. code-block:: bash 68 | 69 | $ pytest -d --tx ssh=myhostpopen --rsyncdir paczka 70 | 71 | 72 | Uruchamianie testów na zdalnych serwerach poprzez socket 73 | 74 | .. code-block:: bash 75 | 76 | $ pytest -d --tx socket=192.168.1.102:8888 --rsyncdir paczka 77 | 78 | 79 | Uruchamianie testów na wielu platformach jednoczesnie 80 | 81 | .. code-block:: bash 82 | 83 | $ pytest -d --dist=each --tx=cos1 --tx=cos2 84 | 85 | 86 | Określanie środowiska testowego w pliku ``.ini`` 87 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 88 | 89 | Można również ustawić automatyczne testowanie w trybie ``looponfailroots`` w pliku ini: 90 | 91 | .. code-block:: bash 92 | 93 | [pytest] 94 | looponfailroots = paczka 95 | 96 | 97 | W pliku ini można ustawić domyśle działanie podczas uruchamiania testu. Można np. ustawić 98 | działanie z trzema podprocesami. 99 | 100 | .. code-block:: bash 101 | 102 | [pytest] 103 | ... 104 | addopts = -n3 105 | addopts = --tx ssh=myhost//python=python2.7 --tx ssh=myhost//python=python3.4 106 | 107 | 108 | Można również ustawić katalogi które chcemy aby były dołączane lub niedołączane podczas 109 | synchronizacji (rsync) przed uruchomieniem testów na zdalnych maszynach. 110 | 111 | .. code-block:: bash 112 | 113 | [pytest] 114 | rsyncdirs = . mypkg helperpkg 115 | rsyncignore = .hg 116 | -------------------------------------------------------------------------------- /page/django/models.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Testowanie modeli 3 | ================= 4 | 5 | Testowanie modeli polega przede wszystkim na sprawdzeniu czy możemy pracować 6 | na utworzonym modelu. Dlatego sprawdzamy możliwość przeprowadzenia podstawowych 7 | operacji CRUD na modelu. 8 | 9 | Podstawy 10 | -------- 11 | 12 | Jeśli posiadamy model który nie zawiera dużej ilości pól z relacjami, możemy do 13 | jego przetestowania wykorzystać `factory-boy` wraz z metodą `clean_fields()`. 14 | 15 | .. code-block:: python 16 | 17 | ... 18 | 19 | def test_create_new_openerrors(self): 20 | open_errors = open_errors_factory.build() 21 | open_errors.clean_fields(exclude=['user']) # EXCLUDE: FK, O2O, M2M Fields 22 | assert open_errors 23 | 24 | Wywołując metodę `clean_fields` sprawdzimy czy pola modelu zawierają wszystkie 25 | zdefiniowane i wymagane parametry (na. max_length dla CharField). Sprawdzimy 26 | również wszystkie walidatory, które zostały dodane do poszczególnych pól. 27 | 28 | Można sprawdzić zapisując obiekt do bazy danych: 29 | 30 | .. code-block:: python 31 | 32 | @pytest.mark.django_db 33 | def test_create_new_openerrors(self, open_errors_factory): 34 | # Create the venue 35 | openerror = open_errors_factory() 36 | 37 | # Check all field and validators 38 | openerror.clean_fields() 39 | 40 | # Check we can find it 41 | openerrors = OpenError.objects.all() 42 | assert len(openerrors) == 1 43 | 44 | first_openerrors = openerrors[0] 45 | assert first_openerrors == openerror 46 | 47 | 48 | Testujemy również wszystkie utworzone metody. 49 | 50 | .. code-block:: python 51 | 52 | @pytest.mark.django_db 53 | class TestOpenErrorsModel: 54 | ... 55 | 56 | def test_check_attribute_in_openerrors(self, open_errors_factory): 57 | # Check attributes 58 | venue = open_errors_factory(name='Wembley Arena') 59 | assert only_venue.name == 'Wembley Arena' 60 | 61 | def test_check_str_representation_of_openerrors(self, open_errors_factory): 62 | # Check string representation 63 | venue = open_errors_factory(name='Wembley Arena') 64 | assert only_venue.__str__() == 'Wembley Arena' # or str(only_venue) 65 | 66 | Jeżeli wykorzystujemy nadpisane metody np. `save()` varto również przetestować 67 | je oddzielnie. 68 | 69 | .. code-block:: python 70 | 71 | class Survey(models.Model): 72 | title = models.CharField(max_length=60) 73 | opens = models.DateField() 74 | closes = models.DateField() 75 | 76 | def save(self, **kwargs): 77 | if not self.pk and not self.closes: 78 | self.closes = self.opens + datetime.timedelta(7) 79 | super(Survey, self).save(**kwargs) 80 | 81 | .. code-block:: python 82 | 83 | @pytest.mark.django_db 84 | class SurveySaveTest: 85 | t = "New Year" 86 | sd = datetime.date(2018, 1, 1) 87 | 88 | def test_closes_autoset(self, survey_factory): 89 | s = survey_factory(title=self.t, opens=self.sd) 90 | assert s.closes == datetime.date(2018, 1, 4)) 91 | 92 | def test_closes_honored(self, survey_factory): 93 | s = survey_factory(title=self.t, opens=self.sd, closes=self.sd) 94 | assert s.closes == self.sd 95 | 96 | def test_closes_reset(self, survey_factory): 97 | s = survey_factory(title=self.t, opens=self.sd) 98 | s.closes = None 99 | with pytest.raises(IntegrityError): 100 | s.save() 101 | 102 | def test_title_only(self): 103 | with pytest.raises(IntegrityError): 104 | Survey.objects.create(title=self.t) 105 | -------------------------------------------------------------------------------- /page/pytest/pytest_faker.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | PyTest Faker 3 | ============ 4 | 5 | ``Faker`` jest pakietem generującym fałszywe dane. Faker może się przydać aby utworzyć 6 | obiekty bazy danych, ładny dokument XML, dane potrzebne do testów czy anonimizacja danych 7 | pobranych z usługi produkcyjnej. 8 | 9 | ``pytest-faker`` jest dodatkiem zapewniającym ndodatkowy fixture będący instancją obiektu faker. 10 | 11 | Instalacja 12 | ---------- 13 | 14 | .. code-block:: bash 15 | 16 | $ pip install pytest-faker 17 | 18 | 19 | Wykorzystanie 20 | ------------- 21 | 22 | Aby utworzyć dane należy utworzyć obiekt klasy Faker, a nas†epnie wywołać jedną z dostępnych 23 | metod na tym obiekcie. Metod jest tak dużo że warto zerknąć do dokumentacji pod adresem 24 | http://faker.readthedocs.io/en/master/providers.html. W niej mamy utworzone grupy w których 25 | mamy wykorzystane konkretne metody. Najczęsciej używane grupy to ``faker.providers.person``, 26 | ``faker.providers.address`` czy ``faker.providers.lorem``. 27 | 28 | 29 | .. code-block:: python 30 | 31 | from faker import Faker 32 | 33 | fake = Faker() 34 | 35 | fake.name() 36 | # 'Lucy Cechtelar' 37 | 38 | fake.address() 39 | # "426 Jordy Lodge 40 | # Cartwrightshire, SC 88120-6700" 41 | 42 | Aby wykorzystać moduł ``pytest-faker`` należy wykorzystać dostarczony fixture. 43 | 44 | .. code-block:: python 45 | 46 | #tests/test_faker.py: 47 | from faker.generator import Generator 48 | 49 | def test_faker(faker): 50 | """Faker factory is a fixture.""" 51 | assert isinstance(faker, Generator) 52 | assert isinstance(faker.name(), str) 53 | 54 | 55 | Lokalizacja 56 | ^^^^^^^^^^^ 57 | 58 | Aby ustawić jezyk w jakim mają zostac wygenerowane dane należy w inicjalizacji 59 | obiektu Faker podać dodatkowy argument będący kodem języku. Język polski jest oznaczony 60 | kodem ``pl_PL``. 61 | 62 | .. code-block:: python 63 | 64 | from faker import Faker 65 | fake = Faker('it_IT') 66 | 67 | for _ in range(10): 68 | print(fake.name()) 69 | 70 | > Elda Palumbo 71 | > Pacifico Giordano 72 | > Sig. Avide Guerra 73 | > Yago Amato 74 | > Eustachio Messina 75 | > Dott. Violante Lombardo 76 | > Sig. Alighieri Monti 77 | > Costanzo Costa 78 | > Nazzareno Barbieri 79 | > Max Coppola 80 | 81 | Aby ustawić lokalizację dla modułu ``pytest-faker`` należy w pliku ``conftest.py`` 82 | nadpisać domyślny fixture ``faker_locale``, który powinien zwracać wartość języka. 83 | 84 | 85 | .. code-block:: python 86 | 87 | # test/conftest.py 88 | @pytest.fixture(scope='session') 89 | def faker_locale(): 90 | return 'pl_PL' 91 | 92 | 93 | Dostęp do losowej instancji 94 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 95 | 96 | Właściwość .random na generatorze zwraca instancję ``random.Random`` używaną do generowania wartości. 97 | 98 | .. code-block:: python 99 | 100 | from faker import Faker 101 | fake = Faker() 102 | 103 | fake.random 104 | fake.random.getstate() 105 | 106 | Domyślnie wszystkie generatory współdzielą to samo wystąpienie ``random.Random``, 107 | do którego można uzyskać dostęp za pomocą ``from faker.generator import random``. 108 | Używanie tego może być przydatne w przypadku wtyczek, które chcą wpływać na wszystkie 109 | instancje fakerów. 110 | 111 | 112 | Seeding generatora 113 | ^^^^^^^^^^^^^^^^^^ 114 | 115 | Kiedy używasz Fakera do testowania, często będziesz chciał wygenerować ten sam zestaw danych. 116 | Dla wygody generator dostarcza również metodę ``seed()``, która zapewnia generowanie 117 | takiego samego zestawu testowego. Wywołanie tych samych metod generatora w tej samej 118 | wersji fakera z taką samą wartością `seed` zwróci nam takie same wyniki. 119 | 120 | .. code-block:: python 121 | 122 | from faker import Faker 123 | fake = Faker() 124 | fake.seed(4321) 125 | 126 | print(fake.name()) 127 | > Margaret Boehm 128 | 129 | Więcej szczegułów można znaleźć w dokumentacji http://faker.readthedocs.io/en/master/index.html#seeding-the-generator -------------------------------------------------------------------------------- /page/django/views.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Testowanie widoków 3 | ================== 4 | 5 | Zadania widoku: 6 | 7 | * gromadzi dane do renderowania przy użyciu metod menedżera 8 | * inicjowanie formularzy 9 | * renderowanie szablonów 10 | 11 | 12 | W widokach nie powinieneś: 13 | 14 | * sprawdzanie poprawności danych - za to jest odpowiedzialność formularz 15 | * zapisywać danych - to jest odpowiezialność formularza 16 | * budowanie złożonych zapytań - to jest odpowiedzialność menedżera 17 | 18 | 19 | Jak testować: 20 | 21 | * Testując widoki wykorzystuj RequestFactory 22 | * jeśli możesz wykonuj bezpośrednie wywołanie widoku 23 | * Konfiguruj zależności w sposób jawny (np. request.user, request.session) 24 | 25 | 26 | Podstawy 27 | -------- 28 | 29 | .. code-block:: python 30 | 31 | from django.contrib.auth.models import User 32 | from django.http import HttpResponseRedirect 33 | from django.views.generic import TemplateView 34 | 35 | from .forms import AddTaskForm 36 | 37 | 38 | class AddTaskView(TemplateView): 39 | 40 | template_name = 'tasks/index.html' 41 | form_class = AddTaskForm 42 | users_manager = User.objects 43 | 44 | def get(self, request, *args, **kwargs): 45 | author = request.user 46 | owner = self._get_owner() 47 | form = self.form_class(author=author, owner=owner) 48 | context = { 49 | 'form': form, 50 | 'owner': owner, 51 | 'author': author, 52 | } 53 | return self.render_to_response(context) 54 | 55 | def post(self, request, *args, **kwargs): 56 | author = request.user 57 | owner = self._get_owner() 58 | form = self.form_class(request.POST, author=author, owner=owner) 59 | if form.is_valid(): 60 | obj = form.save() 61 | return HttpResponseRedirect(obj.get_absolute_url()) 62 | context = { 63 | 'form': form, 64 | 'owner': owner, 65 | 'author': author, 66 | } 67 | return self.render_to_response(context) 68 | 69 | def _get_owner(self): 70 | try: 71 | profile_login = self.kwargs['profile'] 72 | except KeyError: 73 | owner = self.request.user 74 | else: 75 | owner = self.users_manager.get(username=profile_login) 76 | return owner 77 | 78 | 79 | .. code-block:: python 80 | 81 | class TestView: 82 | 83 | def setUp(self): 84 | self._form_class = Mock(AddTaskForm) 85 | self._users_manager = Mock(User.objects) 86 | self._view = AddTaskView.as_view( 87 | form_class=self._form_class, 88 | users_manager=self._users_manager) 89 | 90 | def test_should_display_task_creation_form(self): 91 | url = reverse('my_tasks') 92 | request = self._factory.get(url) 93 | request.user = Mock(User) 94 | 95 | response = self._view(request) 96 | 97 | self.assertTrue('form' in response.context_data) 98 | 99 | def test_should_save_form_and_redirect_on_success(self): 100 | url = reverse('my_tasks') 101 | form = self._form_class.return_value 102 | form.is_valid.return_value = True 103 | redirect_url = '/some/url' 104 | obj = form.save.return_value 105 | obj.get_absolute_url.return_value = redirect_url 106 | data = { 107 | 'title': sentinel.title, 108 | } 109 | request = self._factory.post(url, data) 110 | request.user = Mock(User) 111 | 112 | response = self._view(request) 113 | 114 | self.assertTrue(form.save.called) 115 | self.assertTrue(obj.get_absolute_url.called) 116 | self.assertEqual(response.status_code, 302) 117 | self.assertEqual(response['Location'], redirect_url) 118 | 119 | 120 | 121 | request = RequestFactory().get('/fake-path') 122 | view = HelloView.as_view(template_name='hello.html') 123 | response = view(request, name='bob') 124 | -------------------------------------------------------------------------------- /page/django/index.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Testowanie w Django 3 | =================== 4 | 5 | Rozbudowana struktura aplikacji 6 | 7 | .. code-block:: bash 8 | 9 | - app_name/ 10 | ├──__init__.py 11 | ├── apps.py # rejestracja aplikacji 12 | ├── models.py # modele aplikacji 13 | ├── admin.py # admin panel 14 | ├── urls.py # linki do widoków oraz router 15 | ├── views.py # widoki aplikacji 16 | ├── forms.py # formularze 17 | ├── managers.py # managery i querysety dla modeli 18 | ├── serializers.py # serializatory do modelów 19 | ├── services.py # domenowa logika aplikacji 20 | ├── context_processors.py # kontekst procesory dla szablonów 21 | ├── middleware.py # własne middleware 22 | ├── task.py # zadania do celery 23 | ├── signals/ # sygnały 24 | │ ├── __init__.py 25 | │ ├── signals.py # miejsce do tworzenia własnych sygnałów 26 | │ └── handlers.py # miejsce do odbierania sygnałów 27 | ├── templates/ 28 | │ └── app_name/ # dodanie "przestrzeni nazw" dla templatów 29 | ├── templatetags/ # tagi szablonów 30 | │ ├── __init__.py 31 | │ └── ... 32 | ├── migrations/ # migracje 33 | │ ├── __init__.py 34 | │ ├── 0001_initial.py 35 | │ └── ... 36 | ├── management/ # komendy django uruchamiane z bash 37 | │ ├── __init__.py 38 | │ └── commands/ 39 | │ ├── __init__.py 40 | │ └── ... 41 | └── tests/ # katalog z testami 42 | ├── __init__.py 43 | ├── conftest.py # ustawienia dla pytest, fixtures 44 | ├── factories.py # fabryki obiektów dla modeli 45 | ├── test_forms.py # testy formularzy 46 | ├── test_models.py # testy modeli 47 | ├── test_admin.py # testy admin panelu 48 | ├── test_views.py # testy widoków 49 | ├── test_serializers.py # testy serializatorów 50 | ├── ... # generalnie tworzymy plik odpowiadający testowanemu modułowi 51 | └── functional/ # testy funkcjonalne (WebTest, Django Client, Selenium) 52 | ├── __init__.py 53 | ├── view_board_topics.py 54 | └── view_home.py 55 | 56 | 57 | W jaki sposób najlepiej pisać testy? 58 | 59 | Pytanie nie jest banalne i na początku sprawiało mi to dużo trudności. Pierwszym problemem był brak wiedzy w jaki sposób 60 | skonfigurować środowisko z Django pozwalające na uruchomienie testów. Kolejny problem, to bardzo często złożoność modeli, 61 | w jaki sposób tworzyć do nich obiekty? Inne problemy z którymi się borykałem, to: sprawdzenie czu url działają poprawnie, 62 | sprawdzenie czy utworzone tagi do szablonów sa poprawne, sprawdzenie czy formularze działają poprawnie. Najtrudniejsze było 63 | jednak testowanie widoków w sposób pozwalający na przetestowanie tylko części funkcjonalności a nie całości. 64 | 65 | Zbierając jednak całą tę wiedzę, dalej miałem problemy z tym, w jaki sposób dobrze poukładać testy. Jak w tym wszystkim 66 | wykorzystać moduł `mock`. 67 | 68 | `Podwójna pętla`, to sposób w którym pierwsze piszemy test funkcjonalny, określający na bardzo wysokim poziomie nasze założenia. 69 | Test ten z założenia powinien być błędny. Następnie tworzymy testy jednostkowe pozwalające określić dokładniej co chcemy 70 | zrobić. Każdorazowe poprawne wykonanie testów jednostkowych, będzie przybliżało nas do poprawnego wykonania testu funkcjonalnego. 71 | 72 | Takie podejście również pozwala nam osiągnąć dwie bardzo ważne rzeczy. Pierwsza z nich to pewność, że finalnie nasza aplikacja 73 | będzie działać (patrząc z poziomu klienta). Wykonując testy funkcjonalne nie wykorzystujemy obiektów `mock` oraz 74 | nie nadpisujemy ustawień bazy danych co daje nam pewność, że wszystko będzie OK. Druga ważna rzecz, to możliwość pisania 75 | szybkich testów jednostkowych, które nie zawsze będą tworzyły zapytania do bazy danych. 76 | 77 | .. toctree:: 78 | :maxdepth: 1 79 | 80 | settings 81 | models 82 | managers 83 | admin 84 | forms 85 | urls 86 | views 87 | middleware 88 | template_tags 89 | django_rest_framework 90 | celery 91 | django_mock 92 | -------------------------------------------------------------------------------- /page/pytest/pytest_benchmark.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Pytest Benchmark 3 | ================ 4 | 5 | Benchmark pozwala w bardzo prosty sposób testować wydajność naszego kodu poprzez wykorzystanie fixture w teście. 6 | Fixture ``benchmark`` jest obiektem wywoływalnym który będzie robił testy wydajnościowe dla każdej przekazanej mu funkcji. 7 | 8 | Co zapewnia `pytest-benchmark`? 9 | 10 | * rozsądne wartości domyślne i automatyczna kalibracja dla mikro testów wydajnościowych 11 | * dobra integracja z pytest 12 | * porównywanie i śledzenie regresji 13 | * wymuszone statystyki 14 | * eksport danych do JSON 15 | 16 | 17 | Instalacja 18 | ---------- 19 | 20 | .. code-block:: bash 21 | 22 | $ pip install pytest-benchmark 23 | 24 | Wykorzystanie 25 | ------------- 26 | 27 | .. code-block:: python 28 | 29 | def something(duration=0.000001): 30 | """ 31 | Function that needs some serious benchmarking. 32 | """ 33 | time.sleep(duration) 34 | # You may return anything you want, like the result of a computation 35 | return 123 36 | 37 | def test_my_stuff(benchmark): 38 | # benchmark something 39 | result = benchmark(something) 40 | 41 | # Extra code, to verify that the run completed correctly. 42 | # Sometimes you may want to check the result, fast functions 43 | # are no good if they return incorrect results :-) 44 | assert result == 123 45 | 46 | Można również dodawać argumenty 47 | 48 | .. code-block:: python 49 | 50 | def test_my_stuff(benchmark): 51 | benchmark(time.sleep, 0.02) 52 | 53 | lub argumenty słów kluczowych 54 | 55 | .. code-block:: python 56 | 57 | def test_my_stuff(benchmark): 58 | benchmark(time.sleep, duration=0.02) 59 | 60 | 61 | Jeśli potrzebujemy dokładnej kontrolia nad przebiegiem testu (np. funkcja konfiguracji czy dokładna kontrola iteracji i przebiegu) 62 | istnieje specjalny tryb `pedantyczny` pozwalający na dokładne określenie parametrów uruchamianego testu. 63 | 64 | .. code-block:: python 65 | 66 | def test_with_setup(benchmark): 67 | benchmark.pedantic(something, setup=my_special_setup, args=(1, 2, 3), kwargs={'foo': 'bar'}, iterations=10, rounds=100) 68 | 69 | 70 | Dodatkowe opcje podczas uruchamiania 71 | ------------------------------------ 72 | 73 | Benchmark zawiera szerek dodatkowych opcji ustawianych podczas uruchamiania testu. Więcej 74 | na ich temat możemy znaleźć w dokumentacji modułu http://pytest-benchmark.readthedocs.io/en/stable/usage.html#commandline-options. 75 | 76 | Marker 77 | ------ 78 | 79 | Pozwala nam ustawić opcje testowania dla testu porównawczego. 80 | 81 | .. code-block:: python 82 | 83 | @pytest.mark.benchmark( 84 | group="group-name", 85 | min_time=0.1, 86 | max_time=0.5, 87 | min_rounds=5, 88 | timer=time.time, 89 | disable_gc=True, 90 | warmup=False 91 | ) 92 | def test_my_stuff(benchmark): 93 | @benchmark 94 | def result(): 95 | # Code to be measured 96 | return time.sleep(0.000001) 97 | 98 | # Extra code, to verify that the run 99 | # completed correctly. 100 | # Note: this code is not measured. 101 | assert result is None 102 | 103 | 104 | Dodatkowe informacje 105 | -------------------- 106 | 107 | Tworząc zapis wyników do JSON możemy dodać dodatkowe informacje do słownika. 108 | 109 | .. code-block:: python 110 | 111 | def test_my_stuff(benchmark): 112 | benchmark.extra_info['foo'] = 'bar' 113 | benchmark(time.sleep, 0.02) 114 | 115 | 116 | Narzędzie Patch 117 | --------------- 118 | 119 | Jeśli potrzebujesz przetestować wewnętrzną funkcję klasy należy wykorzystać ``benchmark_weave``. 120 | 121 | .. code-block:: python 122 | 123 | class Foo(object): 124 | def __init__(self, arg=0.01): 125 | self.arg = arg 126 | 127 | def run(self): 128 | self.internal(self.arg) 129 | 130 | def internal(self, duration): 131 | time.sleep(duration) 132 | 133 | W przypadku testu porównawczego jest to dość trudne do sprawdzenia, jeśli nie ma pełnej 134 | kontroli kodu klasy Foo lub ma on bardzo skomplikowaną konstrukcję. Aby przetestować taką 135 | metodę można wykorzystać eksperymentalne fixture ``benchmark_weave``. Należy jednak 136 | się upewnić że mamy zainstalowany moduł ``aspectlib`` (``pip install aspectlib`` 137 | lub ``pip install pytest-benchmark[aspect]``) 138 | 139 | .. code-block:: python 140 | 141 | def test_foo(benchmark): 142 | benchmark.weave(Foo.internal, lazy=True): 143 | f = Foo() 144 | f.run() 145 | 146 | 147 | Zrzuty ekranu 148 | ------------- 149 | 150 | .. image:: benchmark.jpg 151 | 152 | .. image:: benchmark-compare.jpg -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Python Testing documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Feb 9 20:12:10 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | import sphinx_rtd_theme 23 | 24 | sys.path.append(os.path.abspath(os.path.join(os.path.abspath('.'), '../scripts/'))) 25 | 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.doctest', 39 | 'sphinx.ext.todo', 40 | 'sphinx.ext.viewcode', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'Python Testing' 57 | copyright = '2018, Witold Greń' 58 | author = 'Witold Greń' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = '1.0' 66 | # The full version, including alpha/beta/rc tags. 67 | release = '1.0' 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = 'pl' 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '*.psd'] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = True 86 | 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = "sphinx_rtd_theme" 94 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | # 100 | html_theme_options = {} 101 | 102 | # Add any paths that contain custom static files (such as style sheets) here, 103 | # relative to this directory. They are copied after the builtin static files, 104 | # so a file named "default.css" will overwrite the builtin "default.css". 105 | html_static_path = ['_static'] 106 | 107 | # Custom sidebar templates, must be a dictionary that maps document names 108 | # to template names. 109 | # 110 | # This is required for the alabaster theme 111 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 112 | html_sidebars = { 113 | '**': [ 114 | 'relations.html', # needs 'show_related': True theme option to display 115 | 'searchbox.html', 116 | ] 117 | } 118 | 119 | 120 | # -- Options for HTMLHelp output ------------------------------------------ 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = 'PythonTestingdoc' 124 | 125 | 126 | # -- Options for LaTeX output --------------------------------------------- 127 | 128 | latex_elements = { 129 | # The paper size ('letterpaper' or 'a4paper'). 130 | # 131 | # 'papersize': 'letterpaper', 132 | 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | 137 | # Additional stuff for the LaTeX preamble. 138 | # 139 | # 'preamble': '', 140 | 141 | # Latex figure (float) alignment 142 | # 143 | # 'figure_align': 'htbp', 144 | } 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, 148 | # author, documentclass [howto, manual, or own class]). 149 | latex_documents = [ 150 | (master_doc, 'PythonTesting.tex', 'Python Testing Documentation', 151 | 'Witold Greń', 'manual'), 152 | ] 153 | 154 | 155 | # -- Options for manual page output --------------------------------------- 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | (master_doc, 'pythontesting', 'Python Testing Documentation', 161 | [author], 1) 162 | ] 163 | 164 | 165 | # -- Options for Texinfo output ------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | (master_doc, 'PythonTesting', 'Python Testing Documentation', 172 | author, 'PythonTesting', 'One line description of project.', 173 | 'Miscellaneous'), 174 | ] 175 | 176 | # -- Options for Epub output ---------------------------------------------- 177 | 178 | # Bibliographic Dublin Core info. 179 | epub_title = project 180 | epub_author = author 181 | epub_publisher = author 182 | epub_copyright = copyright 183 | epub_language = 'pl' 184 | epub_cover = ('_static/epub_cover.png', '') 185 | 186 | # The unique identifier of the text. This can be a ISBN number 187 | # or the project homepage. 188 | # 189 | # epub_identifier = '' 190 | 191 | # A unique identification for the text. 192 | # 193 | # epub_uid = '' 194 | 195 | # A list of files that should not be packed into the epub file. 196 | epub_exclude_files = ['search.html'] 197 | -------------------------------------------------------------------------------- /page/django/django_rest_framework.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | Testowanie w Django REST framework 3 | ================================== 4 | 5 | Aby przetestować działanie naszego API najprościej jest wykorzystać testu 6 | funkcjonalne. Jednak warto również przetestować działanie naszych serializatorów 7 | (jeśli posiadają nie standardową logikę) lub poszczególnych metody wykorzystywanych 8 | w klasach `APIView` lub `ViewSet`. 9 | 10 | 11 | Sprawdzamy czy jest zwracana poprawna odpowiedź poprzez klienta, są to testy 12 | funkcjonalne wykorzystujące klienta WebTest. 13 | 14 | .. code-block:: python 15 | 16 | class ProductAPITests: 17 | 18 | def test_can_get_product_details(self, django_app, product_factory): 19 | product = product_factory() 20 | response = django_app.get(f'/products/{product.id}/') 21 | assert response.status_code == 200 22 | assert response.data == ProductSerializer(instance=product).data 23 | 24 | def test_can_delete_product(self, django_app, product_factory): 25 | product = product_factory() 26 | response = django_app.delete(f'/products/{product.id}/delete/') 27 | assert response.status_code == 204 28 | assert Product.objects.count() == 0 29 | 30 | def test_can_update_product(self, django_app, product_factory): 31 | product = product_factory() 32 | response = django_app.patch_json(f'/products/{product.id}/update/', params={'name': 'Samsung Watch'}) 33 | product.refresh_from_db() 34 | self.assertEqual(response.status_code, 200) 35 | self.assertEqual(product.name, 'Samsung Watch') 36 | 37 | 38 | Testowanie widoków poprzez APIRequestFactory 39 | -------------------------------------------- 40 | 41 | Możemy również napisać testy, tylko i wyłącznie dla konkretnego widoku z pominięciem 42 | całego stosu zapytania i odpowiedzi (pomijając np. middleware, czy router dla adresu url). 43 | 44 | .. code-block:: python 45 | 46 | from rest_framework import status, viewsets 47 | 48 | class UserContractViewSet(viewsets.ReadOnlyModelViewSet): 49 | serializer_class = UserContractSerializer 50 | 51 | def get_queryset(self): 52 | if self.request.user.is_staff: 53 | return UserContract.objects.all() 54 | return UserContract.objects.filter(user=self.request.user) 55 | 56 | 57 | .. code-block:: python 58 | 59 | @pytest.mark.django_db 60 | def test_user_can_see_own_contracts(api_rf, user_factory, user_contract_factory): 61 | view = UserContractViewSet.as_view({'get': 'list'}) 62 | user = user_factory() 63 | user_contracts = user_contract_factory.build_batch(3, user=user) 64 | 65 | request = api_rf.get('/user_contracts/') 66 | force_authenticate(request, user=user) 67 | response = view(request) 68 | 69 | assert response.status_code == status.HTTP_200_OK 70 | assert response.data.get('count') == 3 71 | assert response.data.get("results") == UserContractSerializer( 72 | user_contracts, many=True).data 73 | 74 | 75 | .. code-block:: python 76 | 77 | @pytest.mark.django_db 78 | def test_user_can_see_own_single_contract(api_rf, user_factory, user_contract_factory): 79 | view = UserContractViewSet.as_view({'get': 'retrieve'}) 80 | user = user_factory() 81 | user_contract = user_contract_factory(user=user) 82 | 83 | request = api_rf.get(f'user_contracts/{user_contract.pk}') 84 | force_authenticate(request, user=user) 85 | response = view(request, pk=user_contract.pk) 86 | 87 | assert response.status_code == status.HTTP_200_OK 88 | assert response.data == UserContractSerializer(user_contract).data 89 | 90 | 91 | Testowanie dekoratora actions 92 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 93 | 94 | Bardzo często korzystając z `ViewSet` można stworzyć dodatkowe metody wywoływane 95 | poprzez url dla obiektu lub listy obiektów. Warto również je przetestować 96 | wywołując odpowiednio api widoku. 97 | 98 | 99 | .. code-block:: python 100 | 101 | class UserViewSet(ModelViewSet): 102 | ... 103 | 104 | @action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf]) 105 | def set_password(self, request, pk=None): 106 | ... 107 | 108 | .. code-block:: python 109 | 110 | @pytest.mark.django_db 111 | def test_user_set_password(api_rf, user_factory): 112 | view = UserViewSet.as_view({'get': 'set_password'}) 113 | user = user_factory() 114 | 115 | request = api_rf.post_json( 116 | f'/user_contracts/{user_contract.pk}/set_password', 117 | params={'password': '123123'}) 118 | force_authenticate(request, user=user) 119 | response = view(request, pk=user.pk) 120 | 121 | assert response.status_code == status.HTTP_200_OK 122 | 123 | 124 | Testowanie routera oraz url 125 | --------------------------- 126 | 127 | Widoki funkcyjne 128 | ^^^^^^^^^^^^^^^^ 129 | 130 | .. code-block:: python 131 | 132 | from chat.views import get_chats 133 | 134 | found = resolve(reverse('referrals')) 135 | assert found2.func.__name__ == get_chats.__name__ 136 | 137 | 138 | Widoki klasowe 139 | ^^^^^^^^^^^^^^ 140 | 141 | .. code-block:: python 142 | 143 | def test_check_if_recent_url_exist_and_have_good_class(self): 144 | found = resolve('/notifications/recent/') 145 | assert found.func.cls == views.UserNotification 146 | 147 | 148 | Przykłady ViewSet dla wybranych akcji 149 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 150 | 151 | .. code-block:: python 152 | 153 | router = DefaultRouter() 154 | router.register(r'my-list', MyViewSet, base_name="my_list") 155 | 156 | urlpatterns = [ 157 | url(r'^api/', include(router.urls, namespace='api')) 158 | ] 159 | 160 | .. code-block:: python 161 | 162 | def test_color_field_content(self): 163 | # for list URL. e.g. /api/my-list/ 164 | path = 'api:my_list-list' 165 | assert reverse(path) == '/api/my-list/' 166 | 167 | found = resolve(reverse(path)) 168 | assert found2.func.__name__ == get_chats.__name__ 169 | 170 | def test_color_field_content(self): 171 | # for detail URL. e.g. /api/my-list// 172 | path = 'api:my_list-detail' 173 | assert reverse(path, args=[1]) == '/api/my-list/1/' 174 | 175 | found = resolve(reverse(path)) 176 | assert found.func.cls == views.UserNotification 177 | 178 | 179 | Testowanie serializatora 180 | ------------------------ 181 | 182 | Testując widok sprawdzamy czy zwrócowna wartość wykorzystuje konkretny serializator. 183 | Nie sprawdzamy jednak samego działania serializatora, nie wiemy czy dodaliśmy do niego 184 | nowe pola, czy może nie zmieniliśmy akcji utworzenia nowego obiektu. Jeśli nasz 185 | serializator posiada co najmniej jedną rzecz, która powoduje, że mamy jakieś 186 | ograniczenia na polu lub podmieniamy domyślną metodę, wtedy musimy przetestować 187 | serializator. 188 | 189 | Poniższy przykład pokazuje bardzo prosty serializator, jednak jak zobaczysz w testach, 190 | jest kilka rzeczy które warto sprawdzić. 191 | 192 | .. code-block:: python 193 | 194 | from django.db import models 195 | 196 | class Tool(models.Model): 197 | COLOR_OPTIONS = ( 198 | ('yellow', 'Yellow'), 199 | ('red', 'Red'), 200 | ('black', 'Black') 201 | ) 202 | 203 | color = models.CharField( 204 | max_length=255, 205 | null=True, 206 | blank=True, 207 | choices=COLOR_OPTIONS) 208 | size = models.DecimalField( 209 | max_digits=4, 210 | decimal_places=2, 211 | null=True, 212 | blank=True) 213 | 214 | 215 | Do podanego powyżej modelu tworzymy prosty serializator. 216 | 217 | .. code-block:: python 218 | 219 | from rest_framework import serializers 220 | from tools.models import Tool 221 | 222 | class ToolSerializer(serializers.ModelSerializer): 223 | COLOR_OPTIONS = ('yellow', 'black') 224 | 225 | color = serializers.ChoiceField( 226 | choices=COLOR_OPTIONS) 227 | size = serializers.FloatField( 228 | min_value=30.0, 229 | max_value=60.0) 230 | 231 | class Meta: 232 | model = Tool 233 | fields = ['color', 'size'] 234 | 235 | 236 | Najpierw przygotowujemy naszą klasę, która będzie zawierać podstawowe dane. 237 | W każdej chwili tworzą test, będziemy mogli je podmienić. 238 | 239 | .. code-block:: python 240 | 241 | @pytest.mark.django_db 242 | class TestToolSerializer: 243 | 244 | @pytest.fixture(autouse=True) 245 | def setup_method(self, db, tool_factory): 246 | self.tool_attributes = { 247 | 'color': 'yellow', 248 | 'size': Decimal('52.12')} 249 | 250 | self.serializer_data = { 251 | 'color': 'black', 252 | 'size': 51.23} 253 | 254 | self.tool = tool_factory(**self.tool_attributes) 255 | self.serializer = ToolSerializer(instance=self.tool) 256 | 257 | 258 | Używam zbioru pól aby upewnić się, że dane wyjściowe z serializera mają 259 | dokładnie te pola, którychy oczekujemy. Używanie zbioru do tej weryfikacji jest 260 | bardzo ważne, ponieważ gwarantuje, że dodanie lub usunięcie dowolnego pola 261 | do serializera zostanie zauważone podczas wykonywania testów. 262 | 263 | .. code-block:: python 264 | 265 | def test_contains_expected_fields(self): 266 | data = self.serializer.data 267 | assert set(data.keys()) == set(['color', 'size']) 268 | 269 | Teraz przechodzimy do sprawdzania, czy serialalizator generuje oczekiwane dane 270 | do każdego pola. Pole kolor jest dość standardowe: 271 | 272 | .. code-block:: python 273 | 274 | def test_color_field_content(self): 275 | data = self.serializer.data 276 | assert data['color'] == self.tool_attributes['color'] 277 | 278 | def test_size_field_content(self): 279 | data = self.serializer.data 280 | assert data['size'] == float(self.tool_attributes['size']) 281 | 282 | 283 | Atrybut `size` ma zarówno dolny, jak i górny limit. Bardzo ważne jest 284 | testowanie przypadków brzegowych które określiliśmy. 285 | 286 | .. code-block:: python 287 | 288 | def test_size_lower_bound(self): 289 | self.serializer_data['size'] = 29.9 290 | 291 | serializer = ToolSerializer(data=self.serializer_data) 292 | 293 | assert serializer.is_valid() is False 294 | assert set(serializer.errors) == set(['size']) 295 | 296 | def test_size_upper_bound(self): 297 | self.serializer_data['size'] = 60.1 298 | 299 | serializer = ToolSerializer(data=self.serializer_data) 300 | 301 | assert serializer.is_valid() is False 302 | assert set(serializer.errors) == set(['size']) 303 | 304 | def test_float_data_correctly_saves_as_decimal(self): 305 | self.serializer_data['size'] = 31.789 306 | 307 | serializer = ToolSerializer(data=self.serializer_data) 308 | serializer.is_valid() 309 | 310 | new_tool = serializer.save() 311 | new_tool.refresh_from_db() 312 | 313 | assert new_tool.size == Decimal('31.79') 314 | 315 | def test_color_must_be_in_choices(self): 316 | self.tool_attributes['color'] = 'red' 317 | 318 | serializer = ToolSerializer(instance=self.tool, data=self.tool_attributes) 319 | 320 | assert serializer.is_valid() is False 321 | assert set(serializer.errors.keys()) == set(['color']) 322 | 323 | 324 | Do przygotowania 325 | ---------------- 326 | 327 | - Testowanie walidatorów oraz walidacji pól 328 | - Testowanie własnych pól 329 | -------------------------------------------------------------------------------- /page/django/django_webtest.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Django WebTest 3 | ============== 4 | 5 | WebTest jest frameworkiem do pisania testów funkcjonalnych/integracyjnych czy akceptacyjnych. 6 | Pierwotnie został napisany dla środowiska ``Pyramid`` jednak można go wykorzystywać również 7 | w innych frameworkach napisanych w języku python. WebTest posiada proste API, jest szybki, 8 | komunikuje się z aplikacją django przez WSGI (nie przez HTTP). Wykorzystując WebTest 9 | dostarcza również mechanizm bardzo dobrej pracy z odpowiedzią otrzymanego widoku. 10 | 11 | **Dlaczego WebTest a nie Django Client?** 12 | 13 | Poniższy kod przedstawia utworzenie testu poprzez wykorzystanie `Django Client`. 14 | 15 | .. code-block:: python 16 | 17 | url = "/case/edit/{0}".format(case.pk) 18 | step = case.steps.get() 19 | response = self.client.post(url, { 20 | "product": case.product.id, 21 | "name": case.name, 22 | "description": case.description, 23 | "steps-TOTAL_FORMS": 2, 24 | "steps-INITIAL_FORMS": 1, 25 | "steps-MAX_NUM_FORMS": 3, 26 | "steps-0-step": step.step, 27 | "steps-0-expected": step.expected, 28 | "steps-1-step": "Click link.", 29 | "steps-1-expected": "Account active.", 30 | "status": case.status, 31 | }) 32 | 33 | 34 | Drugi szablon, przedstawia wykonanie tego samego testu poprzez `WebTest`. 35 | 36 | .. code-block:: python 37 | 38 | url = "/case/edit/{0}".format(case.pk) 39 | form = self.app.get(url).forms["case-form"] 40 | form["steps-1-step"] = "Click link." 41 | form["steps-1-expected"] = "Account active." 42 | form["product"] = case.product.id 43 | form["name"] = case.name 44 | form["description"] = case.description 45 | form["steps-TOTAL_FORMS"] = 2 46 | form["steps-INITIAL_FORMS"] = 1 47 | form["steps-MAX_NUM_FORMS"] = 3 48 | form["steps-0-step"] = step.step 49 | form["steps-0-expected] = step.expected 50 | form["steps-1-step"] = "Click link." 51 | form["steps-1-expected"] = "Account active." 52 | form["status"] = case.status 53 | response = form.submit() 54 | 55 | Problem z utworzeniem testu poprzez django client polega na tym że może on pokazać nie 56 | poprawne wykonanie testu. Wystarczy, że ktoś w szablonie html w którym znajduje się 57 | formularz, przypadkiem usunie tag z specialnymi danymi dla ``formset`` wtedy test 58 | zostanie poprawnie zaliczony, pomimo iż realnie nie działa. W przypadku wykorzystania 59 | WebTest otrzymamy błąd o brakujących danych. Niestety przekazywane wartości poprzez 60 | klient django pomijają renderowanie szablonu, co jest problemem podczas testowania 61 | aplikacji. 62 | 63 | .. note:: 64 | 65 | Więcej szczegółów można znaleść pod adresami: 66 | https://github.com/django-webtest/django-webtest oraz 67 | https://docs.pylonsproject.org/projects/webtest/en/latest/. 68 | 69 | 70 | Instalacja 71 | ---------- 72 | 73 | .. code-block:: bash 74 | 75 | $ pip install django-webtest 76 | 77 | 78 | Opis działania 79 | -------------- 80 | 81 | Domyślne metody to ``django_app.get()`` oraz ``django_app.post()`` czy ``django_app.post_json()`` z opcjonalnym argumentem ``user``. 82 | Wywołanie metody ``django_app.reset()`` powoduje wyczyszczenie wszystkich ciasteczek oraz 83 | wylogowanie użytkownika. 84 | 85 | Aby sprawdzić status odpowiedzi: 86 | 87 | .. code-block:: python 88 | 89 | >>> assert response.status == '200 OK' 90 | >>> assert response.status_int == 200 91 | 92 | Nagłówki odpowiedzi: 93 | 94 | .. code-block:: python 95 | 96 | >>> assert response.content_type == 'text/html' 97 | >>> assert response.content_type == 'application/json' 98 | >>> assert response.content_length > 0 99 | 100 | Treść odpowiedzi: 101 | 102 | .. code-block:: python 103 | 104 | >>> resp.mustcontain('') # zwraca błąd jeśli nie znaleziono łańcucha znaków 105 | >>> assert 'form' in response 106 | >>> assert response.json == {'id': 1, 'value': 'value'} 107 | >>> assert response.text == '' 108 | >>> assert response.body == '' 109 | 110 | Można również pobrać ``request`` z odpowiedzi jednak jest on klasą ``webob.request.BaseRequest`` [#f1]_: 111 | 112 | 113 | .. code-block:: python 114 | 115 | >>> response.request.url 116 | >>> response.request.remote_addr 117 | 118 | 119 | Korzystając z django-webtest otrzymujesz również zmienne ``response.templates`` oraz ``response.context``, z których 120 | można skorzystać w taki sam sposób jak z klienta django. Atrybuty te zawierają listę szablonów wykorzystanych do renderowania odpowiedzi 121 | oraz kontekst używany do renderowania tych szablonów. 122 | 123 | Sesja jest dostępna pod ``django_app.session``. Domyślnie WebTest w każdym zapytaniu w formularzy automatycznie również 124 | wysyła zmienną ``CSRF``. Aby go wyłączyć należy skorzystać z fikstury ``django_app_factory`` i przekazać parametr ``csrf_checks=False``. 125 | 126 | 127 | Zmienne z Django Client 128 | ----------------------- 129 | 130 | .. code-block:: python 131 | 132 | # response.templates 133 | >>> assert response.templates.name == 'index.html' 134 | 135 | # response.context 136 | >>> assert response.context['user'] == user_obj 137 | 138 | response.status_code # --> response.status_int 139 | response.content # --> response.body 140 | response.url # --> response['location'] 141 | response._charset # --> response.charset 142 | response.client # --> return django.test.Client 143 | 144 | 145 | Parsowanie odpowiedzi 146 | --------------------- 147 | 148 | ``response.html`` - zwraca obiekt ``BeautifulSoup``, którego można wykorzystać w testach 149 | 150 | .. code-block:: python 151 | 152 | def test_login(django_app): 153 | resp = django_app.get('/') 154 | assert resp.html.find("a", title="Login").href == "/login/" 155 | 156 | 157 | ``response.xml`` - zwraca obiekt ``ElementTree`` 158 | 159 | .. code-block:: python 160 | 161 | def test_response_from_user_info(django_app): 162 | resp = django_app.get('/user/info/') 163 | assert resp.xml[0].text == 'hey!' 164 | 165 | 166 | ``response.lxml`` - zwraca obiekt ``lxml`` 167 | 168 | .. code-block:: python 169 | 170 | def test_response_from_user_info(django_app): 171 | resp = django_app.get('/user/info/') 172 | assert resp.lxml.xpath('//body/div')[0].text == 'hey!' 173 | 174 | 175 | ``response.pyquery`` - zwraca obiekt ``PyQuery`` (inna implementacja obsługi dokumentów xml) 176 | 177 | .. code-block:: python 178 | 179 | def test_response_from_user_info(django_app): 180 | resp = django_app.get('/user/info/') 181 | assert resp.pyquery('message').text() == 'hey!' 182 | 183 | 184 | ``response.json`` - zwraca obiekt ``simplejson`` 185 | 186 | .. code-block:: python 187 | 188 | def test_login(django_app): 189 | resp = django_app.get('/') 190 | assert sorted(res.json.values()) == [1, 2] 191 | 192 | 193 | Django Rest Framework 194 | --------------------- 195 | 196 | Aby skorzystać z WebTest wraz z ``django rest framework`` należy utworzyć własną 197 | implementację autoryzacji użytkownika. Dzięku temu, podając w zapytaniu ``user=str(user.username)`` 198 | użytkownik zostanie poprawnie zalogowany bez zbędnych dodatkowo wykonywanych metod. 199 | 200 | Najpierw musimy przygotować moduł autoryzacyjny. Należy utworzyć plik ``webtest.py`` 201 | najlepiej w katalogu konfiguracji projektu. Następnie dodajemy do niego poniższą zawartość: 202 | 203 | .. code-block:: python 204 | 205 | # config/webtest.py 206 | from rest_framework.compat import authenticate 207 | from rest_framework.authentication import BaseAuthentication 208 | 209 | 210 | class WebTestAuthentication(BaseAuthentication): 211 | """ 212 | Auth backend for tests that use webtest with Django Rest Framework. 213 | """ 214 | header = 'WEBTEST_USER' 215 | 216 | def authenticate(self, request): 217 | assert ValueError('exist') 218 | value = request.META.get(self.header) 219 | if value: 220 | user = authenticate(django_webtest_user=value) 221 | if user and user.is_active: 222 | return user, None 223 | 224 | Po utworzeniu pliku należy dodać utworzoną metodę autoryzacji do ``Django Rest Framework``. 225 | Aby tego dokonać w pliku settings dodajemy naszą klasę ``WebTestAuthentication`` do słownika 226 | z kluczem ``DEFAULT_AUTHENTICATION_CLASSES``. 227 | 228 | .. code-block:: python 229 | 230 | # settings.py 231 | if ENVIRONMENT == 'tests': 232 | REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] += [ 233 | 'config.webtest.WebTestAuthentication', 234 | ] 235 | 236 | To wystarczy aby utworzyć test wraz z podaniem zalogowanego użytkownika. 237 | 238 | 239 | .. code-block:: python 240 | 241 | # test 242 | resp = app.post_json('/resource/', params=dict(id=1, value='value'), user=str(user.username)) 243 | 244 | 245 | 246 | Praca z błędami odpowiedzi 247 | -------------------------- 248 | 249 | Domyślnie jeśli w naszej odpowiedzi uzyskamy status odpowiedzi będący w innym przedziale 250 | niż 200 <= STATUS < 400 zostanie podniesiony błąd. Aby świadomie przetestować takie rodzaje 251 | odpowiedzi musimy złapać wyjątek ``webtest.AppError`` oraz sprawdzić jego status odpowiedzi. 252 | 253 | .. note:: 254 | 255 | Więcej szczegółów można znaleść pod adresami: 256 | https://stackoverflow.com/questions/21829962/test-for-http-405-not-allowed 257 | http://webtest.pythonpaste.org/en/latest/testapp.html#what-is-tested-by-default 258 | 259 | 260 | .. code-block:: python 261 | 262 | def test_mainpage_post(self): 263 | with pytest.raises(webtest.AppError) as exc: 264 | response = self.testapp.post('/') 265 | 266 | assert str(exc).startswith('Bad response: 405') 267 | 268 | 269 | def test_mainpage_post(self): 270 | response = self.testapp.post('/', expect_errors=True) 271 | assert response.status_int == 405 272 | 273 | 274 | def test_mainpage_post(self): 275 | response = self.testapp.post('/', status=405) 276 | 277 | 278 | Przykład wykorzystania 279 | ---------------------- 280 | 281 | .. code-block:: python 282 | 283 | def test_login(django_app): 284 | resp = django_app.get('/') 285 | assert resp.html.find("a", title="Login").href == "/login/" 286 | 287 | 288 | .. code-block:: python 289 | 290 | def test_login_with_app_factory(django_app_factory): 291 | app = django_app_factory(csrf_checks=False, extra_environ={}) 292 | resp = app.get('/') 293 | assert resp.html.find("a", title="Login").href == "/login/" 294 | 295 | 296 | .. code-block:: python 297 | 298 | def test_blog(django_app): 299 | # pretend to be logged in as user `kmike` and go to the index page 300 | index = django_app.get('/', user='kmike') 301 | 302 | # All the webtest API is available. For example, we click 303 | # on a Blog link, check that it 304 | # works (result page doesn't raise exceptions and returns 200 http 305 | # code) and test if result page have 'My Article' text in 306 | # its body. 307 | assert 'My Article' in index.click('Blog') 308 | 309 | .. code-block:: python 310 | 311 | def test_login(django_app): 312 | form = django_app.get(reverse('auth_login')).form 313 | form['username'] = 'foo' 314 | form['password'] = 'bar' 315 | response = form.submit().follow() 316 | assert response.context['user'].username == 'foo' 317 | 318 | 319 | .. [#f1] `https://docs.pylonsproject.org/projects/webob/en/latest/api/request.html`_ 320 | 321 | .. _`https://docs.pylonsproject.org/projects/webob/en/latest/api/request.html` : https://docs.pylonsproject.org/projects/webob/en/latest/api/request.html 322 | -------------------------------------------------------------------------------- /page/django/pytest_django.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Pytest Django 3 | ============= 4 | 5 | ``pytest-django`` jest dodatkiem do ``pytest``, udostępniającą zestaw przydatnych narzędzi 6 | do testowania aplikacji i projektów Django. Bez tego dodatku bardzo ciężko będzie nam 7 | przetestować architekturę django bez wykorzystywania ``mock``. 8 | 9 | 10 | Instalacja 11 | ---------- 12 | 13 | .. code-block:: bash 14 | 15 | $ pip install pytest-django 16 | 17 | 18 | Konfiguracja 19 | ------------ 20 | 21 | W katalogu głównym naszej aplikacji w pliku ``pytest.ini`` dodajemy zmienną 22 | ``DJANGO_SETTINGS_MODULE`` w której ustawiamy śieżkę do ustawień django. 23 | 24 | .. code-block:: bash 25 | 26 | [pytest] 27 | DJANGO_SETTINGS_MODULE=config.settings.test 28 | 29 | Istnieją jeszcze inne sposoby konfiguracji których opis można znaleść pod adresem: 30 | http://pytest-django.readthedocs.io/en/latest/configuring_django.html 31 | 32 | 33 | Uruchomienie testów 34 | ------------------- 35 | 36 | Testy uruchamiamy w taki sam sposób jak testy w ``pytest``. Mamy natomiast możliwość 37 | uruchomienia ``manage.py test`` a w tle uruchamiamy ``pytest``. Aby tego dokonać tworzymy 38 | plik ``runner.py`` i w środku zamiszczamy poniższy kod: 39 | 40 | .. code-block:: python 41 | 42 | class PytestTestRunner(object): 43 | """Runs pytest to discover and run tests.""" 44 | 45 | def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs): 46 | self.verbosity = verbosity 47 | self.failfast = failfast 48 | self.keepdb = keepdb 49 | 50 | def run_tests(self, test_labels): 51 | """Run pytest and return the exitcode. 52 | 53 | It translates some of Django's test command option to pytest's. 54 | """ 55 | import pytest 56 | 57 | argv = [] 58 | if self.verbosity == 0: 59 | argv.append('--quiet') 60 | if self.verbosity == 2: 61 | argv.append('--verbose') 62 | if self.verbosity == 3: 63 | argv.append('-vv') 64 | if self.failfast: 65 | argv.append('--exitfirst') 66 | if self.keepdb: 67 | argv.append('--reuse-db') 68 | 69 | argv.extend(test_labels) 70 | return pytest.main(argv) 71 | 72 | Następnie należy ustawić zmienną ``TEST_RUNNER`` w pliku ``settings.py``. 73 | 74 | .. code-block:: python 75 | 76 | TEST_RUNNER = 'my_project.runner.PytestTestRunner' 77 | 78 | 79 | Teraz możemy uruchomić nasze testy w podobny sposób w jaki urucamia się je normalnie w django. 80 | 81 | .. code-block:: bash 82 | 83 | $ ./manage.py test -- 84 | 85 | 86 | .. attention:: 87 | 88 | W takiej konfiguracji nie działają parametry ``--ds`` oraz ``--dc``. Należy ustawić 89 | te parametry w pliku ``pytest.ini`` lub wykorzystać zmienną ``--settings`` znaną z komend django. 90 | 91 | 92 | Dodatkowe komendy 93 | ^^^^^^^^^^^^^^^^^ 94 | 95 | Uruchamiająć nasze testy mamy możliwość utworzenia dodatkowych komend. 96 | 97 | ``--fail-on-template-vars`` dzięki której zostanie podniesiony wyjątek dla niepoprawnych zmiennych w szablonach django. 98 | 99 | ``--reuse-db`` - ponownie wykorzystanie testowej bazy danych pomiędzy kolejnymi testami. 100 | Testowa baza danych nie zostanie usunięta, ponowne uruchomienie testu spowoduje wykorzystanie tej bazy. 101 | Opcja ta nie będzie przechwytywać zmian schematu między testami. Można tę opcję użyć razem z 102 | `--create-db` aby ponownie utworzyć bazę danych zgodnie z nowym schematem. 103 | 104 | ``--create-db`` - wymuszenie ponownego utworzenia testowej bazy danych, niezależnie od tego, czy istnieje, czy nie 105 | 106 | ``--nomigrations`` - spowoduje wyłączenie migracji Django 107 | 108 | ``--migrations`` - wymusi utworzenie bazy wraz z migracjami 109 | 110 | Więcej szczegółów na temat konfiguracji i pracy z bazą danych można znalść pod adresem 111 | http://pytest-django.readthedocs.io/en/latest/database.html 112 | 113 | 114 | Markery 115 | ------- 116 | 117 | pytest-django zapewnia kilka bardzo przydatnych markerów które można wykorzyastać podczas pisania testów. 118 | Wszystkie poniższe znaczniki można wykorzystać na funkcji lub klasie testujacej. 119 | 120 | @pytest.mark.django_db(transaction=False) 121 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 122 | 123 | Dzięki temu markerowi uzyskujemy dostęp do bazy danych w testach. Każdy test zostanie 124 | przeprowadzony w ramach własnej transakcji, która zostanie wycofana po zakończeniu testu. 125 | Ustawienie zmiennej ``transaction`` na ``False`` - domyślne zachowanie - powoduje że nasz 126 | test zachowuje się w taki sam sposób jak wykorzystanie ``django.test.TestCase``. Ustawienie 127 | tej zmiennej na ``True`` powoduje zmianę zachowania na identyczną jak w ``django.test.TransactionTestCase``. 128 | 129 | .. note:: 130 | 131 | Aby uzyskać dostęp do bazy danych wewnątrz własnego ``fixture`` należy wykorzystać fixture ``db`` lub ``transactional_db``. 132 | 133 | 134 | .. code-block:: python 135 | 136 | @pytest.mark.django_db 137 | def test_something(): 138 | obj = MyObject.objects.get(id=1) 139 | assert obj.name == 'name' 140 | 141 | @pytest.mark.django_db 142 | class TestUsers: 143 | 144 | def test_my_user(self): 145 | me = User.objects.get(username='me') 146 | assert me.is_superuser 147 | 148 | class TestUsers: 149 | pytestmark = pytest.mark.django_db 150 | 151 | def test_my_user(self): 152 | me = User.objects.get(username='me') 153 | assert me.is_superuser 154 | 155 | 156 | @pytest.mark.urls 157 | ^^^^^^^^^^^^^^^^^ 158 | 159 | Zastąpienie domyślnej konfiguracji url w django 160 | 161 | .. code-block:: python 162 | 163 | @pytest.mark.urls('myapp.test_urls') 164 | def test_something(client): 165 | assert 'Success!' in client.get('/some_url_defined_in_test_urls/').content 166 | 167 | 168 | @pytest.mark.ignore_template_errors 169 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 170 | 171 | Ignorowanie ​​niepoprawnych zmiennych w szablonie 172 | 173 | .. code-block:: python 174 | 175 | @pytest.mark.ignore_template_errors 176 | def test_something(client): 177 | client('some-url-with-invalid-template-vars') 178 | 179 | 180 | Fixtures 181 | -------- 182 | 183 | pytest-django zapewnia kilka fixture które w znaczącym stopniu ułatwiają korzystanie 184 | z wbudowanych w django dodatkowych narzędzi do testowania. 185 | 186 | rf 187 | ^^ 188 | 189 | ``rf`` jest instancją ``django.test.RequestFactory``, która jest wykorzystywana do pisania 190 | testów widoków bez przechodzenia przez wszystkie middleware. Dzięki temu narzędziowi możemy 191 | przetestować konkretną zmienną lub metodę w klasie widoku. 192 | 193 | 194 | .. code-block:: python 195 | 196 | from myapp.views import my_view 197 | 198 | def test_details(rf): 199 | request = rf.get('/customer/details') 200 | response = my_view(request) 201 | assert response.status_code == 200 202 | 203 | 204 | client 205 | ^^^^^^ 206 | 207 | ``client`` jest instancją ``django.test.Client``. Można go wykorzystwać do pisania testów 208 | integracyjnych jedna jest on nie polecany. Zamiast niego lepiej jest skorzystać z modułu ``WebTest``, 209 | który również został opisany. 210 | 211 | .. code-block:: python 212 | 213 | def test_with_client(client): 214 | response = client.get('/') 215 | assert response.content == 'Foobar' 216 | 217 | 218 | admin_client 219 | ^^^^^^^^^^^^ 220 | 221 | ``admin_client`` jest instancją ``django.test.Client`` zalogowanego jako administrator. 222 | 223 | .. code-block:: python 224 | 225 | def test_an_admin_view(admin_client): 226 | response = admin_client.get('/admin/') 227 | assert response.status_code == 200 228 | 229 | 230 | admin_user 231 | ^^^^^^^^^^ 232 | 233 | ``admin_user`` jest obiektem użytkownika (administratora) utworzonego w bazie danych. 234 | Jego nazwa to ``admin`` a hasło ``password``. 235 | 236 | 237 | django_user_model 238 | ^^^^^^^^^^^^^^^^^ 239 | 240 | ``django_user_model`` jest modelem (nie instakcją) użytkownika ustawionego poprzez settings.AUTH_USER_MODEL. 241 | 242 | .. code-block:: python 243 | 244 | def test_new_user(django_user_model): 245 | django_user_model.objects.create(username="someone", password="something") 246 | 247 | 248 | django_username_field 249 | ^^^^^^^^^^^^^^^^^^^^^ 250 | 251 | Jest to fixture wyodrębniający nazwę pola, używaną dla nazwy użytkownika w modelu użytkownika. 252 | Pobranie ustawienia z settings.USERNAME_FIELD. 253 | 254 | 255 | db 256 | ^^ 257 | 258 | fixture który powinien być wykorzystywany tylko w innym fixture który wymaga dostępu do bazy danych. 259 | 260 | 261 | transactional_db 262 | ^^^^^^^^^^^^^^^^ 263 | 264 | fixture który powinien być wykorzystywany tylko w innym fixture który wymaga dostępu do bazy danych. 265 | 266 | 267 | live_server 268 | ^^^^^^^^^^^ 269 | 270 | Ten fixture uruchamia aplikację django w oddzielnym wątku. Dostęp do adresu url można uzyskać 271 | poprzez komendę ``live_server.url``. Ten fixture będzie przydatny w przypadku kiedy będzimy 272 | chcieli uruchomić testy poprz wykorzystanie biblioteki ``selenium``. 273 | 274 | 275 | settings 276 | ^^^^^^^^ 277 | 278 | Ten fixture zapewnia możliwość modyfikacji ustawień Django oraz automatycznie 279 | przywróci wszelkie zmiany dokonane w ustawieniach (modyfikacje, dodatki i usunięcia). 280 | 281 | .. code-block:: python 282 | 283 | def test_with_specific_settings(settings): 284 | settings.USE_TZ = True 285 | assert settings.USE_TZ 286 | 287 | 288 | django_assert_num_queries 289 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 290 | 291 | Ten fixture pozwala sprawdzić oczekiwaną liczbę zapytań do bazy danych. 292 | Obecnie jest obsługiwana tylko domyślną baza danych. 293 | 294 | .. code-block:: python 295 | 296 | def test_queries(django_assert_num_queries): 297 | with django_assert_num_queries(3): 298 | Item.objects.create('foo') 299 | Item.objects.create('bar') 300 | Item.objects.create('baz') 301 | 302 | 303 | mailoutbox 304 | ^^^^^^^^^^ 305 | 306 | Skrzynka nadawcza wiadomości e-mail, do której wysyłane są e-maile generowane przez Django. 307 | 308 | .. code-block:: python 309 | 310 | from django.core import mail 311 | 312 | def test_mail(mailoutbox): 313 | mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com']) 314 | assert len(mailoutbox) == 1 315 | m = mailoutbox[0] 316 | assert m.subject == 'subject' 317 | assert m.body == 'body' 318 | assert m.from_email == 'from@example.com' 319 | assert list(m.to) == ['to@example.com'] 320 | 321 | 322 | Metoda setUp znana z UnitTest 323 | ----------------------------- 324 | 325 | Niestety największym dotąd nie rozwiązanym problemem jest brak możliwości tworzenia 326 | obiektów w bazie danych z wykorzystaniem metody `setup_class` a znanej z biblioteki 327 | UnitTest pod nazwą `setUpClass`. 328 | 329 | `setUpClass()` ==> `setup_class` 330 | `tearDownClass()` ==> `teardown_class` 331 | 332 | Natomiast aby skorzystać z znanej metody `setUp()` oraz skorzystać z bazy danych 333 | do tworzenia obiektów, należy nieco zmienić działanie. 334 | Oba poniższe przykłady działają dokładnie tak samo. 335 | 336 | .. code-block:: python 337 | 338 | class AnimalTestCase(TestCase): 339 | 340 | def setUp(self): 341 | Animal.objects.create(name="lion", sound="roar") 342 | Animal.objects.create(name="cat", sound="meow") 343 | 344 | def test_animals_can_speak(self): 345 | """Animals that can speak are correctly identified""" 346 | lion = Animal.objects.get(name="lion") 347 | cat = Animal.objects.get(name="cat") 348 | self.assertEqual(lion.speak(), 'The lion says "roar"') 349 | self.assertEqual(cat.speak(), 'The cat says "meow"') 350 | 351 | 352 | .. code-block:: python 353 | 354 | @pytest.mark.django_db 355 | class AnimalTestCase: 356 | 357 | @pytest.fixture(autouse=True) 358 | def setup_method(self, db): 359 | Animal.objects.create(name="lion", sound="roar") 360 | Animal.objects.create(name="cat", sound="meow") 361 | 362 | def test_animals_can_speak(self): 363 | """Animals that can speak are correctly identified""" 364 | lion = Animal.objects.get(name="lion") 365 | cat = Animal.objects.get(name="cat") 366 | assert lion.speak() == 'The lion says "roar"' 367 | assert cat.speak() == 'The cat says "meow"' 368 | 369 | 370 | https://stackoverflow.com/questions/34089425/django-pytest-setup-method-database-issue 371 | https://github.com/pytest-dev/pytest-django/issues/297 372 | -------------------------------------------------------------------------------- /page/django/settings.rst: -------------------------------------------------------------------------------- 1 | Ustawienia 2 | ========== 3 | 4 | Testując aplikację lokalnie bardzo ważne jest aby testy uruchamiały się bardzo szybko, 5 | sprawia to, że nasza uwaga jest poświęcona cały czas na pisaniu dobrego kodu. Aby 6 | przyspieszyć wykonywanie testów w Django istnieje kilka dobrych praktyk które spowodują 7 | że testy będą działać odczuwalnie szybciej. 8 | 9 | 10 | Zmień hashowanie hasła 11 | ---------------------- 12 | 13 | Jest to najskuteczniejsze ustawienie, które można wykorzystać do poprawy szybkości testów. 14 | Może się to wydawać śmieszne, ale hashowanie haseł w Django jest bardzo mocne, dlatego 15 | korzysta on z kilku "hasherów", oznacza to jednak, że haszowanie jest bardzo powolne. 16 | Najszybszym hasherem jest ``MD5PasswordHasher``, dlatego warto go użyć podczas testowania 17 | aplikacji: 18 | 19 | .. code-block:: python 20 | 21 | PASSWORD_HASHERS = ( 22 | 'django.contrib.auth.hashers.MD5PasswordHasher', 23 | ) 24 | 25 | 26 | Użyj SQLite w pamięci 27 | --------------------- 28 | 29 | Obecnie najszybszą bazą danych z której korzysta Django jest SQLite. Testujemy własną 30 | implementację kodu, własne API i jeśli nie używamy surowych zapytań SQL, 31 | bazowy mechanizm przechowywania danych nie powinien pokazywać różnic! 32 | 33 | Warto więc zmienić go na silnik ``SQLite``: 34 | 35 | .. code-block:: python 36 | 37 | # test_settings.py 38 | DATABASES = { 39 | 'default': { 40 | 'ENGINE': 'django.db.backends.sqlite3', 41 | 'NAME': ':memory:', 42 | } 43 | } 44 | 45 | .. code-block:: python 46 | 47 | #if manage.py test was called, use test settings 48 | if 'test' in sys.argv: 49 | try: 50 | from test_settings import * 51 | except ImportError: 52 | pass 53 | 54 | .. attention:: 55 | 56 | Jeśli wykorzystujemy `continuous integration` nie powinniśmy podmieniać ustawień 57 | bazy danych. Takie środowisko powinno być najbardziej zbliżone do środowiska 58 | produkcyjnego. 59 | 60 | Również jeśli wykonujemy testy integracyjne (nie powinny one być połączone z testami 61 | jednostkowymi w tym samym pliku) nie powinniśmy również zmieniać ustawień bazy danych. 62 | 63 | .. note:: 64 | 65 | Jeśli wykorzystujemy specyficzne rozwiązania z silnika bazy danych z której korzystamy, 66 | możemy tagować nasze testy markerami, zapewni nam to możliwość uruchomienia testów 67 | specyficznych dla danej bazy danych oraz do szybkie testowanie zapytań napisanych 68 | w Django ORM. 69 | 70 | 71 | .. code-block:: python 72 | 73 | import pytest 74 | 75 | @pytest.mark.postgres 76 | class TestSpecificForPostgreSQL(TestCase): 77 | 78 | def test_save_json_field_from_api(self): 79 | ... 80 | 81 | 82 | Warto w ustawieniach dodać brak możliwości podmiany ustawień bazy danych podczas 83 | wykonywania testów. 84 | 85 | .. code-block:: python 86 | 87 | # test_settings.py 88 | 89 | if not 'not postgres' in sys.argv and 'test' in sys.argv: 90 | DATABASES = { 91 | 'default': { 92 | 'ENGINE': 'django.db.backends.sqlite3', 93 | 'NAME': ':memory:', 94 | } 95 | } 96 | 97 | Uruchomienie testów jest bardzo proste. Wystarczy w testach podać atrybut uruchamiający 98 | wszystkie testy poza testami z markerem ``postgres``. 99 | 100 | .. code-block:: bash 101 | 102 | $ pytest -v -m "not postgres" 103 | 104 | 105 | Innym sposobem na rozwiązanie tego problemu jest napisanie nakładek na specyficzne pola 106 | dla danego silnika bazy danych. Niestety nie miałem z tym większej styczności dlatego 107 | przekierowuję do jednego z artykułów. 108 | 109 | https://www.aychedee.com/2014/03/13/json-field-type-for-django/ 110 | 111 | 112 | Usuń niepotrzebne middleware 113 | ---------------------------- 114 | 115 | Im więcej klas middleware, tym więcej czasu będzie potrzebne na wygenerowanie odpowiedzi (ponieważ 116 | wszystkie warstwy pośredniczące muszą być wykonywane sekwencyjnie przed zwróceniem ostatecznej 117 | odpowiedzi HTTP). Warto więc uruchomić tylko te warstwy których tak naprawdę potrzebujesz! 118 | 119 | Szczególnie jeden middleware jest bardzo wolny: 120 | 121 | .. code-block:: python 122 | 123 | django.middleware.locale.LocaleMiddleware 124 | 125 | Możemy założyć, że wszystkie middleware z Django działają poprawnie, dlatego podczas 126 | testowania możemy je usunąć, aby uniknąć wszystkich narzutów podczas wysyłania żądań. 127 | 128 | .. code-block:: python 129 | 130 | MIDDLEWARE_CLASSES = [ 131 | 'django.contrib.sessions.middleware.SessionMiddleware', 132 | 'django.middleware.csrf.CsrfViewMiddleware', 133 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 134 | 'django.contrib.messages.middleware.MessageMiddleware', 135 | ] 136 | 137 | 138 | Usuń niepotrzebne aplikacje 139 | --------------------------- 140 | 141 | Istnieje kilka aplikacji, które można usunąć podczas testowania, np. ``django-debug-toolbar`` 142 | czy ``django_extension`` spróbuj usunąć wszystkie nieużywane/niepotrzebne aplikacje podczas 143 | wykonywania testów. 144 | 145 | 146 | Wyłącz debugowanie 147 | ------------------ 148 | 149 | Ustawienie parametru ``DEBUG=False`` podczas uruchamiania testów zmniejsza obciążenie 150 | związane z debugowaniem, dzięki czemu poprawia się szybkość wykonywania testów. 151 | 152 | .. code-block:: python 153 | 154 | DEBUG = False 155 | 156 | 157 | Wyłącz informacje o logach 158 | -------------------------- 159 | 160 | Jest to znacząca modyfikacja tylko wtedy, gdy mamy ogromną ilość logowań i/lub dodatkowej 161 | logiki związanej z logami (np. inspekcje obiektów, ciężkie manipulacje ciągami itd.). 162 | Logowanie również jest niepotrzebne podczas wykonywania testów, dlatego nie ma potrzeby 163 | dodawania dodatkowego narzutu pliku I/O do pakietu testowego. 164 | 165 | .. code-block:: python 166 | 167 | import logging 168 | logging.disable(logging.CRITICAL) 169 | 170 | 171 | Użyj szybszego zaplecza e-mail 172 | ------------------------------ 173 | 174 | Domyślnie Django używa ``django.core.mail.backends.locmem.EmailBackend``, który jest 175 | backendem przeznaczonym do testowania w pamięci, jednak czasem mogą z nim wystąpić problemy 176 | z powodu sprawdzanie nagłówków. Warto więc skorzystąć z alternatywnego backendu mailowego. 177 | 178 | .. code-block:: python 179 | 180 | EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 181 | 182 | 183 | Używaj Celery uruchamianego w pamięci 184 | ------------------------------------- 185 | 186 | Jeśli wykorzystujesz Celery w swoich projektach warto zmienić ustawienia do testowania: 187 | 188 | .. code-block:: python 189 | 190 | CELERY_ALWAYS_EAGER = True 191 | CELERY_EAGER_PROPAGATES_EXCEPTIONS = True 192 | BROKER_BACKEND = 'memory' 193 | 194 | 195 | Mock, mock, mock! 196 | ----------------- 197 | 198 | Wykorzystując ``Mock`` możesz znacznie skrócić czas testowania swoich aplikacji. 199 | Obiekty Mock można używać podczas każdych testów, najeży jednak pamiętać aby nie tworzyć 200 | mocków do bazy danych jeśli nie posiadamy testów integracyjnych. Więcej szczegułów 201 | na temat tworzenia ``Mock`` znajdziesz w module ``pytest-mock``. 202 | 203 | 204 | Dodatkowe opcje 205 | --------------- 206 | 207 | Domyślne ustawienie lokalizacj dla Faker 208 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 209 | 210 | .. code-block:: python 211 | 212 | import pytest 213 | from requests_mock import MockerCore 214 | from factory.faker import Faker 215 | from faker import config 216 | 217 | Faker._DEFAULT_LOCALE = 'pl_PL' 218 | config.DEFAULT_LOCALE = 'pl_PL' 219 | 220 | 221 | Funkcja testująca metody widoków 222 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 223 | 224 | .. code-block:: python 225 | 226 | def setup_view(view, request, *args, **kwargs): 227 | """ 228 | Mimic as_view() returned callable, but returns view instance. 229 | args and kwargs are the same you would pass to ``reverse()`` 230 | 231 | Example: 232 | name = 'django' 233 | request = RequestFactory().get('/fake-path') 234 | view = HelloView(template_name='hello.html') 235 | view = setup_view(view, request, name=name) 236 | 237 | Example test ugly dispatch(): 238 | response = view.dispatch(view.request, *view.args, **view.kwargs) 239 | """ 240 | view.request = request 241 | view.args = args 242 | view.kwargs = kwargs 243 | return view 244 | 245 | 246 | Funkcja testująca metody widoków API 247 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 248 | 249 | .. code-block:: python 250 | 251 | def api_setup_view(view, request, action=None, *args, **kwargs): 252 | """ 253 | request = HttpRequest() 254 | view = views.ProfileInfoView() 255 | view = api_setup_view(view, request, 'list') 256 | assert view.get_serializer_class() == view.serializer_class 257 | """ 258 | view.request = request 259 | view.action = action 260 | view.args = args 261 | view.kwargs = kwargs 262 | return view 263 | 264 | 265 | Klasa APIRequestFactory jako fixture 266 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 267 | 268 | .. code-block:: python 269 | 270 | @pytest.fixture() 271 | def api_rf(): 272 | """ 273 | APIRequestFactory instance 274 | """ 275 | skip_if_no_django() 276 | from rest_framework.test import APIRequestFactory 277 | return APIRequestFactory() 278 | 279 | 280 | Biblioteka requests_mock jako fixture 281 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 282 | 283 | .. code-block:: python 284 | 285 | import pytest 286 | from requests_mock import MockerCore 287 | 288 | # -------------------------------------------------------------------- 289 | # dodatek pozwalający w łatwy sposób robić mock dla biblioteki request 290 | # -------------------------------------------------------------------- 291 | 292 | @pytest.yield_fixture(scope="session") 293 | def requests_mock(): 294 | """ 295 | def test_get_tags(self, requests_mock): 296 | requests_mock.get(settings.MY_SERVICE + 'tag/', json=response) 297 | cron = ImportTriviaCromJob() 298 | assert list(cron.get_tags(name)) == result 299 | """ 300 | mock = MockerCore() 301 | mock.start() 302 | yield mock 303 | mock.stop() 304 | 305 | 306 | Fixture dla DjangoLiveServer w kontenerze Docker 307 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 308 | 309 | .. code-block:: python 310 | 311 | import pytest 312 | from pytest_django.lazy_django import skip_if_no_django 313 | from pytest_django.live_server_helper import LiveServer 314 | 315 | # ------------------------------------------------------------------------ 316 | # dodatek pozwalający na uruchomienie DjangoLiveServer w kontenerze Docker 317 | # ------------------------------------------------------------------------ 318 | 319 | @pytest.fixture(scope='session') 320 | def live_server(request): 321 | server = DockerLiveServer() 322 | request.addfinalizer(server.stop) 323 | return server 324 | 325 | 326 | class DockerLiveServer(LiveServer): 327 | 328 | def __init__(self): 329 | import socket 330 | self.addr = socket.gethostbyname(socket.gethostname()) 331 | 332 | import django 333 | from django.db import connections 334 | from django.test.testcases import LiveServerThread 335 | from django.test.utils import modify_settings 336 | 337 | connections_override = {} 338 | for conn in connections.all(): 339 | # If using in-memory sqlite databases, pass the connections to 340 | # the server thread. 341 | if conn.vendor == 'sqlite' and conn.is_in_memory_db(conn.settings_dict['NAME']): 342 | # Explicitly enable thread-shareability for this connection 343 | conn.allow_thread_sharing = True 344 | connections_override[conn.alias] = conn 345 | 346 | liveserver_kwargs = {'connections_override': connections_override} 347 | from django.conf import settings 348 | if 'django.contrib.staticfiles' in settings.INSTALLED_APPS: 349 | from django.contrib.staticfiles.handlers import StaticFilesHandler 350 | liveserver_kwargs['static_handler'] = StaticFilesHandler 351 | else: 352 | from django.test.testcases import _StaticFilesHandler 353 | liveserver_kwargs['static_handler'] = _StaticFilesHandler 354 | 355 | if django.VERSION < (1, 11): 356 | host, possible_ports = self.addr, [8081] 357 | self.thread = LiveServerThread(host, possible_ports, **liveserver_kwargs) 358 | else: 359 | host = self.addr 360 | self.thread = LiveServerThread(host, **liveserver_kwargs) 361 | 362 | self._live_server_modified_settings = modify_settings( 363 | ALLOWED_HOSTS={'append': host} 364 | ) 365 | self._live_server_modified_settings.enable() 366 | 367 | self.thread.daemon = True 368 | self.thread.start() 369 | self.thread.is_ready.wait() 370 | 371 | if self.thread.error: 372 | raise self.thread.error 373 | 374 | @property 375 | def url(self): 376 | if self.thread.host == self.addr: 377 | return 'http://%s:%s' % ('localhost', self.thread.port) 378 | return 'http://%s:%s' % (self.thread.host, self.thread.port) 379 | -------------------------------------------------------------------------------- /page/pytest/pytest.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | PyTest 3 | ====== 4 | 5 | 6 | Dlaczego pytest 7 | --------------- 8 | 9 | * pozwala pisać małe i łatwe testy 10 | * posiada mnogość pluginów jeszcze bardziej upraszczających testowanie 11 | * można w nim uruchomić również UnitTest 12 | * do porównywania wykorzystujemy tylko słowo ``assert`` a otrzymujemy bardzo szczegółowe informacje o błędach 13 | * posiadamy ``fixture`` - wstrzykiwanie zależności 14 | * posiadamy możliwość tworzenia markerów ``pytest.mark.skipif`` czy ``pytest.mark.xfail`` 15 | * możemy parametryzować testy zmniejszając ilość napisanego kodu 16 | * automatycznie wykrywa moduły testowe, klasy i funkcje bez zbędnego dziedziczenia klas 17 | * ``pytest`` pozwala na konfigurację projektu ``pytest.ini`` oraz każdego modułu w inny sposób poprzez wykorzystanie ``conftest.py`` 18 | * implementacja ``hooks`` 19 | 20 | 21 | .. hint:: 22 | Do czego służą i na co nam pozwalają fixture? 23 | 24 | * unikanie samopowtarzalności w kodzie 25 | * można je w łatwy sposób modyfikować 26 | * wstrzykiwanie zależności 27 | * można je tworzyć w bardzo łatwy sposób 28 | 29 | 30 | .. hint:: 31 | 32 | Co to są markery i jak możemy je wykorzystać? 33 | 34 | * pozwalają kontrolować co ma zostać uruchomione w teście 35 | * `pytest` zawiera wbudowanych kilka markerów np. ``pytest.mark.skipif``, ``pytest.mark.xfail``, ``pytest.mark.parametrize``, ``pytest.mark.tryfirst``, ``pytest.mark.trylast`` i inne. 36 | * można tworzyć swoje markery, podczas uruchamiania testów można oznaczyć które mają zostać uruchomione 37 | 38 | 39 | Instalacja 40 | ---------- 41 | 42 | .. code-block:: bash 43 | 44 | $ pip install pytest 45 | 46 | 47 | Konfiguracja 48 | ------------ 49 | 50 | W katalogu głównym naszej aplikacji tworzymy plik ``pytest.ini`` w którym będzie znajdować 51 | się konfiguracja ``pytest``. Używając ``Django`` plik ``pytest.ini`` powinien znaleźć się 52 | w katalogu w którym mamy umieszczony plik ``manage.py``, Nie jest ona wymagana, 53 | jednak czasem przydaje się aby zmniejszyć ilość wpisywanych komend podczas uruchamiania testów. 54 | Poniżej zamieszczona jest przykładowa konfiguracja: 55 | 56 | .. code-block:: bash 57 | 58 | [pytest] 59 | python_files = tests.py test_*.py 60 | addopts = -s -q --disable-warnings --doctest-modules 61 | norecursedirs = .git .cache tmp* 62 | 63 | 64 | Więcej szczegółów dotyczących konfiguracji można znaleźć w `Konfiguracji pytest` lub w dokumentacji 65 | do poszczególnych pluginów. Przykładowa powyższa konfiguracja zawiera nagłówek ``[pytest]``, 66 | oraz trzy ustawienia: 67 | 68 | * python_files - ustawienie informujące ``pytest`` w jakich plikach ma poszukiwać testów, 69 | * addopts - uruchamiając komendę ``pytest`` nie musimy za każdym razem podawać całego ciągu znaczników którymi chcemy ustawić test, w tym miejscu ustawiamy je jednorazowo i będą one automatycznie dołączane podczas uruchamiania testów. Wyjaśnienia: ``-s`` jest to skrót od ``--capture=no`` który wyłącza przechwytywanie wyjścia komunikatów np. print, ``-q`` zmniejsza szczegółowość danych podczas uruchomienia testu, ``--disable-warnings`` oznacza wyłączenie podsumowania o ostrzeżenie w kodzie, ``--doctest-modules`` uruchamia wszystkie `doctests` we wszystkich plikach ``.py``. 70 | * norecursedirs - informacja które foldery należy wykluczyć podczas poszukiwania plików z testami 71 | 72 | .. tip:: 73 | 74 | Inne popularne ustawienia addopts to: 75 | * ``-x``, ``--exitfirst`` zamknięcie testów podczas pierwszej nieudanej próby wykonania testu 76 | * ``--maxfail=num`` wyjście po przekroczeniau ``num`` ilości błędnych testów 77 | * ``--fixtures`` pokazanie aktualnie dostępnych `fixtures` 78 | * ``--markers`` pokazanie wszystkich zainstalowanych `marks` 79 | * ``--pdb`` uruchomienie debugera kodu 80 | * ``-p no:warnings`` wyłączenie ostrzeżeń podczas testów 81 | * ``-v``, ``-vv``, ``-vvv``, ``-vvvv`` szczegułowość komunikatów o błędach 82 | 83 | 84 | .. tip:: 85 | 86 | Inne ustawienia w pliku ``pytest.ini``: 87 | * ``python_classes = *Suite`` stawienie typu klasy, w której będą poszukiwanie testy 88 | * ``python_functions = *_test`` - ustawienie typu funkcji które będą uruchamiane jako testy 89 | 90 | 91 | .. _`Konfiguracji pytest` : https://docs.pytest.org/en/latest/customize.html 92 | 93 | 94 | Uruchomienie testów 95 | ------------------- 96 | 97 | Uruchomienie ``pytest`` dla konkretnego pliku 98 | 99 | .. code-block:: bash 100 | 101 | $ pytest test_mymodule.py 102 | $ pytest -vsl test_mymodule.py 103 | 104 | 105 | Uruchomienie wszystkiego co ma w nazwie `special_run` 106 | 107 | .. code-block:: bash 108 | 109 | $ pytest -k 'special_run' 110 | 111 | 112 | Uruchomienie testów które są udekorowane wybranym markerem `marker_name` 113 | 114 | .. code-block:: bash 115 | 116 | $ pytest -m 'marker_name' 117 | 118 | 119 | Jeśli posiadamy plugin `xdist` uruchomi on testy na 4 procesorach 120 | 121 | .. code-block:: bash 122 | 123 | $ pytest -n 4 124 | 125 | 126 | Oznaczanie całych klas lub modułów markerem 127 | ------------------------------------------- 128 | 129 | Jeśli utworzymy dekorator markera na klasie, wszystkie testy klasy będą oznaczone tym markerem. 130 | 131 | .. code-block:: python 132 | 133 | # content of test_mark_classlevel.py 134 | import pytest 135 | @pytest.mark.webtest 136 | class TestClass(object): 137 | def test_startup(self): 138 | pass 139 | def test_startup_and_more(self): 140 | pass 141 | 142 | Dla zachowania kompatybilności wstecznej z wersją 2.4 możemy również użyć zmienne 143 | ``pytestmark``. Jest to równoznaczne z utworzeniem dekoratora z markerem na klasie. 144 | 145 | .. code-block:: python 146 | 147 | import pytest 148 | 149 | class TestClass(object): 150 | pytestmark = pytest.mark.webtest 151 | 152 | 153 | Można również podać kilka markerów w liście. 154 | 155 | .. code-block:: python 156 | 157 | import pytest 158 | 159 | class TestClass(object): 160 | pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] 161 | 162 | 163 | Oznaczenie całego modułu markerem można wykonać w następujący sposób. 164 | 165 | .. code-block:: python 166 | 167 | import pytest 168 | pytestmark = pytest.mark.webtest 169 | 170 | 171 | Pisanie własnych fixture 172 | ------------------------ 173 | 174 | W większości frameworków testowych `fixture` są powszechne. Są to w zasadzie 175 | obiekty, które możemy wykorzystać w naszych testach. Ostatecznie zapewniają 176 | stałą linię bazową, na której testy mogą być wykonywane niezawodnie i wielokrotnie. 177 | W pytest fixture, które wykraczają poza typową konfigurację i funkcjonalność. 178 | 179 | - `fixture` posiadają jawne nazwy i są aktywowane poprzez deklarowanie ich 180 | w funkcjach testowych, modułach, klasach lub całych projektach. 181 | - `fixture` są modułowe, a każde `fixture` wyzwala funkcję urządzenia, 182 | które może korzystać z innych `fixture`. 183 | - Możesz sparametryzować `fixture` i testy zgodnie z opcjami konfiguracji 184 | lub ponownie wykorzystać `fixture` w obrębie zakresów klasy, modułu lub 185 | całej sesji testowej. 186 | 187 | 188 | Tworzenie własnego fixture 189 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 190 | 191 | Aby utworzyć własny `fixture` należy wykorzystać dekorator `pytest.fixture`. 192 | 193 | .. code-block:: python 194 | 195 | import pytest 196 | 197 | @pytest.fixture() 198 | def my_fixture(): 199 | print "\nI'm the fixture" 200 | 201 | 202 | Używanie fixture w kodzie 203 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 204 | 205 | Aby użyć wcześniej napisanego `fixture`, wystarczy że przekażemy go jako 206 | parametr w funkcji testowej. Należy pamiętać, że zawsze jako pierwszy zostanie 207 | wykonana funkcja `fixture` a dopiero potem funkcja testująca. 208 | 209 | .. code-block:: python 210 | 211 | def test_my_fixture(my_fixture): 212 | print "I'm the test" 213 | 214 | 215 | Pytest podaje nam kilka innych sposobów korzystania z naszych `fixture`. 216 | Metoda standardowego parametru jest świetna i używana najczęściej, ale mamy 217 | również dekorator `usefixtures()`. 218 | 219 | .. code-block:: python 220 | 221 | @pytest.mark.usefixtures('my_fixture') 222 | def test_my_fixture(): 223 | print "I'm the test" 224 | 225 | Oznaczamy test, aby użyć naszego `fixture`, a wyniki jego działania są takie 226 | same jak wcześniej. Warto zwrócić uwagę, że można przekazać wiele `fixture` 227 | za pomocą wartości rozdzielanych przecinkami. Metoda ta jest przydatna w 228 | klasach testowych. 229 | 230 | .. code-block:: python 231 | 232 | @pytest.mark.usefixtures('my_fixture', 'my_fixture2') 233 | class Test: 234 | def test1(self): 235 | print "I'm the test 1" 236 | 237 | def test2(self): 238 | print "I'm the test 2" 239 | 240 | 241 | Używamy `fixture` dla całej klasy, a następnie każdy test w klasie użyje tego `fixture`. 242 | Oszczędza to czas oznaczania wszystkich testów, jeśli mają korzystać z tego 243 | samego urządzenia. Innym sposobem uzyskania tego samego efektu na całym pliku 244 | testowym jest ustawienie zmiennej `pytestmark`. 245 | 246 | .. code-block:: python 247 | 248 | import pytest 249 | 250 | pytestmark = pytest.mark.usefixtures('my_fixture') 251 | 252 | def test_my_fixture(): 253 | print "I'm the test" 254 | 255 | class Test: 256 | def test1(self): 257 | print "I'm the test 1" 258 | 259 | def test2(self): 260 | print "I'm the test 2" 261 | 262 | W ten sposób ustawiamy `fixture` globalnie dla tego pliku, a wszystkie 263 | funkcje testowe znajdujące się w nim, będą go używać. 264 | 265 | Należy pamiętać, że wszystkie funkcje testowe mogą nie wymagać tego `fixture`. 266 | Jeśli tak jest, lepiej jest bezpośrednio określić każdy `fixture` osobno dla 267 | funkcji testującej, zamiast wybierać leniwe drogi i oznaczać je z góry na 268 | wszystkich funkcjach. W przypadku większych `fixture` może to spowodować, 269 | że testy będą ładować się wolniej. 270 | 271 | 272 | Ostatnim sposobem użycia `fixture`, jest ustawienie parametru `autouse` w deklaracji `fixture`. 273 | `Fixture` będzie automatycznie wywoływany bez jawnego deklarowania argumentów 274 | funkcji lub dekoratora usefixtures. 275 | 276 | `Fixture` - `transact` na poziomie klasy jest oznaczone jako `autouse=True`, co 277 | oznacza, że ​​wszystkie metody testowe w klasie będą używać tego `fixture` bez 278 | potrzeby podawania go w sygnaturze funkcji testowej lub przy użyciu dekoratora 279 | klasy używanej na poziomie klasy. 280 | 281 | .. code-block:: python 282 | 283 | import pytest 284 | 285 | class DB(object): 286 | def __init__(self): 287 | self.intransaction = [] 288 | def begin(self, name): 289 | self.intransaction.append(name) 290 | def rollback(self): 291 | self.intransaction.pop() 292 | 293 | @pytest.fixture(scope="module") 294 | def db(): 295 | return DB() 296 | 297 | class TestClass(object): 298 | 299 | @pytest.fixture(autouse=True) 300 | def transact(self, request, db): 301 | db.begin(request.function.__name__) 302 | yield 303 | db.rollback() 304 | 305 | def test_method1(self, db): 306 | assert db.intransaction == ["test_method1"] 307 | 308 | def test_method2(self, db): 309 | assert db.intransaction == ["test_method2"] 310 | 311 | 312 | Używanie `autouse` może być wspaniałe, ale może być również niebezpieczne, 313 | jak pokazano w ostatnim przykładzie. `Autouse`, o ile nie jest ograniczone do zakresu, 314 | będzie działać na wszystkich testach w bieżącej sesji. 315 | 316 | Praca i zakres `autofocus`: 317 | 318 | - ustawienia `autouse=True` jest zgodne z `scope=`argument: jeśli `fixture` 319 | ma `scope='session'`, to zostanie ono uruchomione tylko raz, bez względu na to, 320 | gdzie zostało ono zdefiniowane. 321 | scope = 'class' oznacza, że ​​będzie uruchamiany raz na klasę, itd. 322 | - jeśli zdefiniowano `fixture` z parametrem `autouse=True` w module testowym, 323 | wszystkie jego funkcje testowe automatycznie będą go używać. 324 | - jeśli zdefiniowano `fixture` z parametrem `autouse=True` w pliku conftest.py, 325 | wówczas wszystkie testy we wszystkich modułach testowych poniżej jego katalogu wywołają tego `fixture`. 326 | 327 | 328 | Warto zauważyć, że powyższy `fixture` z argumentem `autouse` również może zostać zwykłym `fixture`, 329 | którego można użyć w projekcie bez automatycznej aktywacji. Kanonicznym sposobem 330 | na to, jest umieszczenie definicji transakcji w pliku conftest.py 331 | bez użycia funkcji `autouse=True`: 332 | 333 | .. code-block:: python 334 | 335 | # content of conftest.py 336 | @pytest.fixture 337 | def transact(self, request, db): 338 | db.begin() 339 | yield 340 | db.rollback() 341 | 342 | a następnie np. w klasie `TestClass`, deklarujesz jego użycie: 343 | 344 | .. code-block:: python 345 | 346 | @pytest.mark.usefixtures("transact") 347 | class TestClass(object): 348 | def test_method1(self): 349 | ... 350 | 351 | Wszystkie metody testowe w tej klasie testowej będą używać `fixture` transakcyjnego, 352 | podczas gdy inne klasy testowe lub funkcje w tym samym module nie będą go używać. 353 | 354 | 355 | Zwracanie wartości 356 | ^^^^^^^^^^^^^^^^^^ 357 | 358 | `fixture` są używane przede wszystkim do zwracania danych, którymi można manipulować podczas testów. 359 | Tak jak zwykła funkcja, możemy zwrócić coś, a następnie w naszym teście możemy z tego skorzystać. 360 | 361 | .. code-block:: python 362 | 363 | import pytest 364 | 365 | @pytest.fixture() 366 | def my_fixture(): 367 | data = {'x': 1, 'y': 2, 'z': 3} 368 | return data 369 | 370 | def test_my_fixture(my_fixture): 371 | assert my_fixture['x'] == 1 372 | 373 | 374 | Dodawanie finalizerów 375 | ^^^^^^^^^^^^^^^^^^^^^ 376 | 377 | Jeśli chcesz uruchomić coś po zakończeniu testu z `fixture`, możesz użyć finalizatorów. 378 | W tym celu uzyskujemy dostęp do `request fixture` z pytest. 379 | Finalizator to funkcja wewnątrz `fixture`, która będzie uruchomiona po każdym teście, 380 | w którym znajduje się dany `fixture`. 381 | 382 | .. code-block:: python 383 | 384 | @pytest.fixture() 385 | def my_fixture(request): 386 | data = {'x': 1, 'y': 2, 'z': 3} 387 | 388 | def fin(): 389 | print "\nMic drop" 390 | request.addfinalizer(fin) 391 | 392 | return data 393 | 394 | `request` posiada metodę `addfinalizer()`, która może przyjąć funkcję. 395 | Nasza funkcja może po prostu wypisywać coś na ekranie lub np. możemy odłączyć 396 | się od bazy danych. Daje nam to kontrolę nad `fixture` po zakończeniu testu, 397 | które go wykorzystuje. 398 | 399 | Zakres fixture 400 | ^^^^^^^^^^^^^^ 401 | 402 | Wielokrotnie możemy chcieć mieć `fixture`, który chcemy uruchomić na przykład na wszystkich 403 | funkcjach lub we wszystkich klasach. Pytest podaje nam zestaw kilka zmiennych, które dokładnie określają zakres, 404 | kiedy chcemy korzystać z naszego `fixture`. 405 | 406 | - `function`: uruchomienie `fixture` jeden raz na przypadek testowy 407 | - `class`: uruchomienie jeden raz na klasę 408 | - `module`: uruchomienie jeden raz na moduł 409 | - `session`: uruchomienie jeden raz na sesję 410 | 411 | Aby z nich skorzystać, definiujemy argument `scope`. 412 | 413 | .. code-block:: python 414 | 415 | @pytest.fixture(scope="class") 416 | 417 | Domyślnie `scope` jest ustawione na `function`. Gdzie chciałbyś użyć każdego z nich? 418 | 419 | - Możesz użyć `function`, jeśli chcesz, aby urządzenie działało po każdym pojedynczym teście. 420 | Jest to dobre rozwiązanie w przypadku utworzenia małych `fixture`. 421 | - Zakres `class`, jest wykorzystywany jeśli chcesz, aby działał on w każdej klasie. 422 | Zazwyczaj grupujemy testy w jednej klasie kiedy są podobne. Ten zakres jest wykorzystywany 423 | właśnie wtedy kiedy chcemy wykonać coś jeden raz dla całej grupy testów. 424 | - Zakres `module`, można użyć jeśli chcemy, aby `fixture` był uruchamiany na początku 425 | bieżącego pliku, a następnie zakończony po uruchomieniu wszystkich testów znajdujących się wewnątrz pliku. 426 | Ten zakres można wykorzystać jeśli masz `fixture`, który uzyskuje dostęp do bazy danych 427 | i konfiguruje bazę danych na początku modułu, a następnie finalizator zamyka połączenie. 428 | - Zakres `session` jest wykorzystywany, jeśli chcemy uruchomić `fixture` w pierwszym teście a następnie 429 | uruchomić finalizator po uruchomieniu ostatniego testu. Jeśli zakres ustawimy na `session` a `autouse=True`, 430 | to nasz `fixture` zostanie uruchomiony tylko na początku sesji. 431 | 432 | 433 | Używanie informacji o fixture w testach 434 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 435 | 436 | Pytest zapewnia dostęp do informacji o `fixture` poprzez wykorzystanie argumentu `request`. 437 | 438 | - `scope`: request.scope 439 | - `function name`: request.function.__name__ 440 | - `class`: request.cls 441 | - `module`: request.module.__name__ 442 | - `filesystem path`: request.fspath 443 | 444 | 445 | Dodawanie parametrów do fixture 446 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 447 | 448 | Pytest zapewnia również wielokrotne używanie pojedynczego `fixture`. 449 | Poprzez przekazanie parametru `params=[]` do definicji `fixture` możemy stworzyć 450 | wiele `fixture`. Poniższy przykład pokazuje jak to zrobić. 451 | 452 | .. code-block:: python 453 | 454 | import pytest 455 | 456 | @pytest.fixture(params=[ 457 | # Tuples with password string and expected result 458 | ('password', False), 459 | ('p@ssword', False), 460 | ('p@ssw0rd', True) 461 | ]) 462 | def password(request): 463 | """Password fixture""" 464 | return request.param 465 | 466 | 467 | def password_contains_number(password): 468 | """Checks if a password contains a number""" 469 | return any([True for x in range(10) if str(x) in password]) 470 | 471 | 472 | def password_contains_symbol(password): 473 | """Checks if a password contains a symbol""" 474 | return any([True for x in '!,@,#,$,%,^,&,*,(,),_,-,+,='.split(',') if x in password]) 475 | 476 | 477 | def check_password(password): 478 | """Check the password""" 479 | return password_contains_number(password) and password_contains_symbol(password) 480 | 481 | 482 | def test_password_verifier_works(password): 483 | """Test that the password is verified correctly""" 484 | (text, result) = password 485 | print '\n' 486 | print text 487 | 488 | assert check_password(text) == result 489 | 490 | Mimo iż uruchomiliśmy tylko jeden test (`test_password_verifier_works`), 491 | w sumie został on uruchomiony trzy krotnie, każdy z innymi wartościami. 492 | 493 | 494 | Pomijanie testów 495 | ---------------- 496 | 497 | Wiele razy wiemy, że test zakończy się niepowodzeniem. W takich przypadkach 498 | chcemy zmodyfikować test lub zmodyfikować kod, jednak nadal posiadanie testu 499 | który nie przechodzi może zablokować zestaw testowy, aby tego uniknąć pytest 500 | daje nam narzędzia `skip` oraz `xfail`, które pozwolą nam kontrolować takie zachowanie. 501 | 502 | `skip` oznacza, że test zostanie uruchomiony, chyba że środowisko (np. nieprawidłowy 503 | interpreter języka Python, brak zależności) zapobiegają jego uruchomieniu. 504 | 505 | `xfail` natomiast oznacza, że test zostanie uruchomiony zawsze, ale spodziewamy 506 | się niepowodzenia, ponieważ wystąpił problem z implementacją. 507 | 508 | .. code-block:: python 509 | 510 | import pytest 511 | import sys 512 | 513 | @pytest.mark.skipif(sys.platform != 'win32', reason="requires windows") 514 | def test_func_skipped(): 515 | """Test the function""" 516 | assert 0 517 | 518 | @pytest.mark.xfail 519 | def test_func_xfailed(): 520 | """Test the function""" 521 | assert 0 522 | 523 | skip 524 | ^^^^ 525 | 526 | Powyżej przeprowadziliśmy dwa testy, jeden został pominięty (ponieważ nie został 527 | uruchomiony w systemie windows), a drugi był nieudany, ponieważ wiedzieliśmy, 528 | że to nie zadziała. Przyjrzyjmy się najpierw pomijaniu testów. 529 | 530 | Podczas pomijania testów, podajemy warunek który musi zostać spełniony. Jeśli 531 | warunek nie zostanie spełniony, test nie zostanie uruchomiony oraz zostanie 532 | oznaczony jako pominięty. Jest to idealne rozwiązanie do testów, które mogą 533 | wymagać konkretnych wersji modułów i oprogramowania. Może to być kłopotliwe, 534 | jeśli mamy szereg testów, które wymagają takiej samej konfiguracji pomijania, 535 | warto stworzyć dekorator ułatwiający oznaczanie testów. 536 | 537 | .. code-block:: python 538 | 539 | import sys 540 | import pytest 541 | 542 | windows = pytest.mark.skipif(sys.platform != 'win32', reason="requires windows") 543 | 544 | @windows 545 | def test_func_skipped(): 546 | """Test the function""" 547 | assert 0 548 | 549 | Możemy zastosować dekorator `@windows` do dowolnej funkcji testującej. 550 | Dodatkowym sposobem pozwalającym na pominięcie jest wykorzystanie `importorskip`. 551 | 552 | .. code-block:: python 553 | 554 | docutils = pytest.importorskip("docutils", minversion="0.3") 555 | 556 | Jeśli nie można zaimportować `docutils`, spowoduje to pominięcie testu. 557 | 558 | 559 | xfail 560 | ^^^^^ 561 | 562 | Wykorzystując `xfail` również możemy skorzystać z podobnych warunków jakie 563 | występują w dekoratorze `skip`. 564 | 565 | .. code-block:: python 566 | 567 | import pytest 568 | import sys 569 | 570 | 571 | @pytest.mark.xfail(sys.version_info >= (3,3), reason="python3.3 api changes") 572 | def test_func_xfailed(): 573 | """Test the function""" 574 | assert 0 575 | 576 | Poniżej znajduje się kila przykładów pokazujących w jaki sposób można wykorzystać 577 | funkcję `xfail`. 578 | 579 | .. code-block:: python 580 | 581 | import pytest 582 | xfail = pytest.mark.xfail 583 | 584 | @xfail 585 | def test_hello(): 586 | assert 0 587 | 588 | @xfail(run=False) 589 | def test_hello2(): 590 | assert 0 591 | 592 | @xfail("hasattr(os, 'sep')") 593 | def test_hello3(): 594 | assert 0 595 | 596 | @xfail(reason="bug 110") 597 | def test_hello4(): 598 | assert 0 599 | 600 | @xfail('pytest.__version__[0] != "17"') 601 | def test_hello5(): 602 | assert 0 603 | 604 | def test_hello6(): 605 | pytest.xfail("reason") 606 | 607 | @xfail(raises=IndexError) 608 | def test_hello7(): 609 | x = [] 610 | x[1] = 1 611 | 612 | Określając `run=False` test nie zostanie uruchomiony. Możemy również użyć 613 | wyrażenia tekstowego jako testu, aby sprawdzić, czy test nie powiedzie się. 614 | Możemy również w samym teście wywołać funkcję `pytest.xfail("reason")`, która 615 | spowoduje, że się nie powiedzie. 616 | 617 | Korzystając z `xfail` i `skip`, możesz podać powód dlaczego test się nie powiedzie 618 | lub dlaczego zostaje on pominięty. Kiedy uruchomimy testy, nie zobaczymy tych powodów. 619 | Aby zobaczyć opisy dla tych funkcji należy uruchomić testy z następującą komendą: 620 | 621 | 622 | .. code-block:: bash 623 | 624 | $ pytest -rxs 625 | 626 | 627 | Parametryzacja testów 628 | --------------------- 629 | 630 | W niektórych przypadkach wystarczy utworzyć jednorazowy test lecz często zdarza 631 | się że chcemy sprawdzić kila przypadków zmieniając wartości wybranych zmiennych. 632 | W takiej sytuacji nie ma potrzeby pisać kolejnych przypadków testowych, ale warto 633 | skorzystać z parametryzacji jednego przypadku testowego. 634 | 635 | .. code-block:: python 636 | 637 | import pytest 638 | 639 | @pytest.mark.parametrize('expression, expected', [ 640 | ('2 + 3', 5), 641 | ('6 - 4', 2), 642 | pytest.mark.xfail(('5 + 2', 8)) 643 | ]) 644 | def test_equations(expression, expected): 645 | """Test that equation works""" 646 | assert eval(expression) == expected 647 | 648 | 649 | Ustawienia xUnit - konfiguracja i odłogowanie 650 | --------------------------------------------- 651 | 652 | Testując w stylu XUnit zawsze wykonujemy ustawienie (`setting up`) oraz 653 | czyszczenie (`tearing down`) przypadków testowych. Pytest również obsługuje 654 | ten styl pisania testów. 655 | 656 | .. code-block:: python 657 | 658 | def setup_module(module): 659 | """Run at the start of a testing module (module)""" 660 | pass 661 | 662 | def teardown_module(module): 663 | """Run at the end of a testing module (file)""" 664 | pass 665 | 666 | def setup_function(function): 667 | """Setup a function""" 668 | pass 669 | 670 | def teardown_function(function): 671 | """Teardown a function""" 672 | pass 673 | 674 | class TestClass: 675 | 676 | @classmethod 677 | def setup_class(cls): 678 | """Setup the class""" 679 | pass 680 | 681 | @classmethod 682 | def teardown_class(cls): 683 | """Teardown the class""" 684 | pass 685 | 686 | def setup_method(self, method): 687 | """Setup a method""" 688 | pass 689 | 690 | def teardown_method(self, method): 691 | """Teardown a method""" 692 | pass 693 | 694 | 695 | Praca z wyjątkami 696 | ----------------- 697 | 698 | Jeśli wiemy, że dany kod powinien podnieść wyjątek i chcemy go przetestować, czy 699 | na pewno został wywołany, musimy użyć funkcji `pytest.raises`. 700 | 701 | .. code-block:: python 702 | 703 | def test_zero_division(): 704 | with pytest.raises(ZeroDivisionError): 705 | 1 / 0 706 | 707 | def test_recursion_depth(): 708 | with pytest.raises(RuntimeError) as excinfo: 709 | def f(): 710 | f() 711 | f() 712 | assert 'maximum recursion' in str(excinfo.value) 713 | 714 | 715 | Przykład pisania kodu 716 | --------------------- 717 | 718 | .. code-block:: python 719 | 720 | class TestCalc: 721 | 722 | def test_add_method(self): 723 | calc = Calc() 724 | assert calc.add(1, 1) == 2 725 | assert calc.add(0, 3) == 3 726 | 727 | 728 | .. code-block:: python 729 | 730 | @pytest.fixture(scope='function') 731 | def calc(request): 732 | c = Calc() 733 | return c 734 | 735 | class TestCalc: 736 | 737 | def test_add_method(self, calc): 738 | assert calc.add(1, 1) == 2 739 | assert calc.add(0, 3) == 3 740 | 741 | 742 | .. code-block:: python 743 | 744 | @pytest.fixture(scope='function') 745 | def calc(request): 746 | c = Calc() 747 | return c 748 | 749 | class TestCalc: 750 | 751 | @pytest.mark.parametrize('a, b, exp', [ 752 | (1, 1, 2), (0, 3, 3) 753 | ]) 754 | def test_add_method(self, calc, a, b, exp): 755 | assert calc.add(a, b) == exp 756 | assert calc.add(a, b) == exp 757 | 758 | 759 | .. code-block:: python 760 | 761 | @pytest.fixture(scope='function') # or 'session' 762 | def calc(request): 763 | c = Calc() 764 | return c 765 | 766 | class TestCalc: 767 | 768 | @pytest.mark.parametrize('a, b, exp', [ 769 | (1, 1, 2), (0, 3, 3) 770 | ]) 771 | def test_add_method(self, calc, a, b, exp): 772 | assert calc.add(a, b) == exp 773 | assert calc.add(a, b) == exp 774 | 775 | # pytest-quickcheck 776 | @pytest.mark.randomize(a=int, ncalls=4) 777 | def test_add_method(self, calc, a): 778 | assert calc.add(a, a) == 2 * a 779 | 780 | 781 | .. code-block:: python 782 | 783 | api_mark = pytest.mark.on_api 784 | local = pytest.mark.local 785 | 786 | @pytest.fixture(scope='session') 787 | def calc(request): 788 | c = Calc() 789 | return c 790 | 791 | @local 792 | class TestCalc: 793 | 794 | @pytest.mark.parametrize('a, b, exp', [ 795 | (1, 1, 2), (0, 3, 3) 796 | ]) 797 | def test_add_method(self, calc, a, b, exp): 798 | assert calc.add(a, b) == exp 799 | assert calc.add(a, b) == exp 800 | 801 | # pytest-quickcheck 802 | @pytest.mark.randomize(a=int, ncalls=4) 803 | def test_add_method(self, calc, a): 804 | assert calc.add(a, a) == 2 * a 805 | 806 | @api_mark 807 | class TestServer: 808 | 809 | def test_on_api(self): 810 | assert False 811 | 812 | # $ pytest -v -m local file_name.py 813 | # $ pytest -v -m on_api file_name.py 814 | 815 | 816 | .. code-block:: python 817 | 818 | api_mark = pytest.mark.on_api 819 | local = pytest.mark.local 820 | 821 | @pytest.fixture(scope='session') 822 | def calc(request): 823 | c = Calc() 824 | return c 825 | 826 | @pytest.fixture(scope='session') 827 | def api(request): 828 | def api_cal(a, b): 829 | res = request.get('http://127.0.0.1:3007/add/', params={'a':a, 'b': b}) 830 | res.raise_for_status() 831 | return res.json() 832 | return api_cal 833 | 834 | @local 835 | class TestCalc: 836 | 837 | @pytest.mark.parametrize('a, b, exp', [ 838 | (1, 1, 2), (0, 3, 3) 839 | ]) 840 | def test_add_method(self, calc, a, b, exp): 841 | assert calc.add(a, b) == exp 842 | assert calc.add(a, b) == exp 843 | 844 | # pytest-quickcheck 845 | @pytest.mark.randomize(a=int, ncalls=4) 846 | def test_add_method(self, calc, a): 847 | assert calc.add(a, a) == 2 * a 848 | 849 | @api_mark 850 | class TestServer: 851 | 852 | def test_on_api(self, api): 853 | assert api(1, 2) == 3 854 | 855 | # $ pytest -v -m local file_name.py 856 | # $ pytest -v -m on_api file_name.py 857 | 858 | 859 | .. code-block:: python 860 | 861 | mode = pytest.mark.mode 862 | 863 | ... 864 | 865 | @mode('local') 866 | class TestCalc: 867 | ... 868 | 869 | @mode('api') 870 | class TestServer: 871 | ... 872 | 873 | # $ pytest -v -R local file_name.py 874 | # $ pytest -v -R api file_name.py 875 | 876 | 877 | .. code-block:: python 878 | 879 | def local_calc(request): 880 | c = Calc() 881 | return x 882 | 883 | def api(request): 884 | def api_cal(a, b): 885 | ... 886 | 887 | @pytest.fixture(scope='session') 888 | def calc(request): 889 | mode = request.config.getoption('-R') 890 | if mode == 'local': 891 | return local_calc(request) 892 | elif mode == 'api': 893 | return api(request) 894 | else: 895 | raise Exception('local or api allowed') 896 | 897 | @mode('local') 898 | class TestCalc: 899 | def test_add(self, calc): 900 | ... 901 | 902 | @mode('api') 903 | class TestServer: 904 | def test_add(self, calc): 905 | ... 906 | -------------------------------------------------------------------------------- /page/pytest/pytest_mock.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Pytest Mock 3 | =========== 4 | 5 | Pytest-mock jest pluginem ułatwiającym tworzenie mocków w testach. Nie musimy importować 6 | modułu Mock, patch i innych, są one dostępne bespośrednio jako fixture. Jednak aby zacząć 7 | z niego korzystać musimy zrozumieć czym jest Mock oraz w jaki wposób działa. Pytest-mock 8 | nie robi żadnej magi wokoło modułu mocków, jednak jeśli nie rozumiemy jak działa obiekt 9 | Mock będziemy mieli problem z zrozumieniem w jaki sposób z niego korzytać. 10 | 11 | Czym jest mokowanie? Jesy to symulowaniem działania obiektu. Mokowanie obiektów jest 12 | bardzo dobrym narzędziem. Jednak należy uważać z jego nadużywaniem. Dobrym miejscem do 13 | ich wykorzystania są: 14 | 15 | * systemowe wywołania (np. ``os.environ``) 16 | * strumienie (np. ``sys.stdout``) 17 | * sieć (np. ``request.open``) 18 | * operacje wejścia/wyjścia (np. ``json.loads``) 19 | * zegar, czas, data (np. ``time.sleep``) 20 | * nieprzewidywalne wyniki (np. ``random.random``) 21 | 22 | .. danger:: 23 | 24 | Złym miejscem wykonywania mocków jest symulowanie działania bazy danych bez utworzenia 25 | testów integracyjnych. Również nie powinno sie dokonywać mokowania bazy danych podczas 26 | wykonywania samych testów integracyjnych. W Django poprzez test integracyjny rozumiemy 27 | korzystanie z narzędzia ``WebTest``, ``Selenium`` czy ``Django Client``. 28 | 29 | 30 | Przykład rozwiązania, które może spowodować problemy podczas testowania aplikacji. 31 | 32 | .. code-block:: python 33 | 34 | class DjangoFakeModel: 35 | def __init__(self, age=None): 36 | self.age = age 37 | 38 | def save_base(self, *args, **kwargs): 39 | assert NotImplementedError('call save_base') 40 | 41 | class DjangoFakeForm: 42 | def __init__(self, instance=None, data=None): 43 | self.instance = instance 44 | self.data = data 45 | 46 | def is_valid(self): 47 | return True 48 | 49 | def save(self): 50 | for key, value in self.data.items(): 51 | setattr(self.instance, key, value) 52 | self.instance.save_base() 53 | return self.instance 54 | 55 | def test_solution1(mocker): 56 | mock_save = mocker.object(DjangoFakeModel, 'save_base') 57 | form = DjangoFakeForm(instance=DjangoFakeModel(), data={'age': 3}) 58 | 59 | assert form.is_valid() 60 | saved_pony = form.save() 61 | 62 | assert saved_pony.age == 3 63 | mock_save.assert_called_once() # <- a raczej powinno być assert_called_once_with 64 | 65 | 66 | Problem z tworzeniem mocka polega na tym, że bardzo łatwo można przez pomyłkę wywołać funckję, 67 | która będzie bardzo podobna do oryginalnej a jednak nie zwróci ona błędu. Przykładem może być 68 | ``mock_save.assart_called_once()``. Wykonanie powyższego testu będzie zawsze poprawne. Na szczęscie 69 | wywołanie na MagicMock metody która rozpoczyna się od ``assert_`` będzie również sprawdzona 70 | poprawność wywołania, co zabezpiecza nas przed popełnieniem błędu. 71 | 72 | 73 | Jak działa Mock? 74 | ---------------- 75 | 76 | Aby skorzystać z obiektu Mock należy go zaimportować. W python 2 importujemy go poprzez ``import mock`` 77 | (wczesniej należy zainstalować bibliotekę ``pip install mock``) natomiast w pythonie 3 78 | importujemy go z modułu unittest ``from unittest import mock``. 79 | 80 | Utworzenie Mock odbywa się poprzez utworzene obiektu klasy Mock. Obiekt ten posiada szczegulną 81 | własność, potrafi w locie utworzyć atrybuty i metody które są mu potrzebne. Warto tworząc 82 | obiekt mock podać atrybut ``name``, dzięki temu będziemy wiedzieli jaki mock aktualnie 83 | jest uruchomiony. 84 | 85 | .. code-block:: python 86 | 87 | >>> m = mock.Mock(name='my_first_mock') 88 | >>> m 89 | # normalnie mamy wartość 90 | 91 | Obiekt Mock zawiera kilka specialnych metod i atrybutów. 92 | 93 | .. code-block:: python 94 | 95 | >>> dir(m) 96 | ['assert_any_call', 'assert_called_once_with', 'assert_called_with', 'assert_has_calls', 97 | 'attach_mock', 'call_args', 'call_args_list', 'call_count', 'called', 'configure_mock', 98 | 'method_calls', 'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value', 'side_effect'] 99 | 100 | Próbując odczytać nie istniejący atrybut nie otrzymamy błedu `AttributeError`, otrzymujemy 101 | kolejny obiekt Mock. Nowy obiekt jest na stałe przypisany do wywołanego atrybutu. 102 | Kilkukrotne wywołanie tego samego atrybutu zawsze zwróci ten sam Mock. 103 | 104 | .. code-block:: python 105 | 106 | >>> m.some_attribute 107 | 108 | >>> dir(m) 109 | ['assert_any_call', 'assert_called_once_with', 'assert_called_with', 'assert_has_calls', 110 | 'attach_mock', 'call_args', 'call_args_list', 'call_count', 'called', 'configure_mock', 111 | 'method_calls', 'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value', 'side_effect', 112 | 'some_attribute'] 113 | 114 | Wywołanie nie istniejącej funkcji o takiej same nazwie jak atrybut zwróci inny obiekt Mock. 115 | 116 | .. code-block:: python 117 | 118 | m.some_attribute() 119 | 120 | 121 | Jak możesz zauważyć, takie obiekty są doskonałym narzędziem do naśladowania innych obiektów, 122 | ponieważ mogą ujawnić dowolny interfejs API bez zgłaszania wyjątków. Jednak aby je wykorzystać 123 | w testach, muszą one zachowywać się tak, jak oryginał, co oznacza że muszą zwracać 124 | rozsądne wartości lub wykonywanie operacje. 125 | 126 | Atrybut ``spec`` 127 | ^^^^^^^^^^^^^^^^ 128 | 129 | Tworząc mock możemy podać atrybut ``spec``. Efektem jego działanie jest utworzenie 130 | obiektu Mock który będzie zawierał takie same metody, właściwości jak wskazany obiekt. 131 | Taki obiekt mock, nie może fałszować dodatkowych atrybutów, które nie znajdują się 132 | w klasie na podstawie której został zbudowany. Warto zwrócić uwagę na fakt, że mock 133 | stworzony na podstawie klasy, która implementuje atrybuty wewnątrz swoich funkcji np. 134 | funkcji `__init__` nie są dostępne w samym obiekcie. 135 | 136 | 137 | .. code-block:: python 138 | 139 | >>> class MySuperClass: 140 | ... def __init__(self, x=0, y=0): 141 | ... self.x = x 142 | ... self.y = y 143 | ... def get_max(): 144 | ... return max(x, y) 145 | >>> m = mock.Mock(spec=MySuperClass) 146 | >>> m.some_attribute.side_effect = lambda x: print(x + 45) 147 | AttributeError: Mock object has no attribute 'some_attribute' 148 | >>> m.get_max() 149 | 150 | >>> m.x 151 | AttributeError: Mock object has no attribute 'x' 152 | 153 | 154 | .. code-block:: python 155 | 156 | class A: 157 | SPECIAL = 1 158 | 159 | def get_special(self): 160 | return self.SPECIAL 161 | 162 | def set_special(self, value): 163 | self.SPECIAL = value 164 | 165 | >>> m2 = mock.Mock(spec=A) 166 | >>> dir(m2) 167 | ['SPECIAL', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', 168 | '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', 169 | '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', 170 | '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 171 | '__subclasshook__', '__weakref__', 'assert_any_call', 'assert_called', 'assert_called_once', 172 | 'assert_called_once_with', 'assert_called_with', 'assert_has_calls', 'assert_not_called', 173 | 'attach_mock', 'call_args', 'call_args_list', 'call_count', 'called', 'configure_mock', 174 | 'get_special', 'method_calls', 'mock_add_spec', 'mock_calls', 'reset_mock', 175 | 'return_value', 'set_special', 'side_effect'] 176 | >>> m2.get_other() 177 | AttributeError ... 178 | 179 | 180 | Atrybut ``return_value`` 181 | ^^^^^^^^^^^^^^^^^^^^^^^^ 182 | 183 | Jest to atrybut dzięki któremu określamy jaka powinna zostać zwrócona wartość dla 184 | wywoływanego atrybutu lub metody. 185 | 186 | .. code-block:: python 187 | 188 | >>> m.some attribute.return_value = 42 189 | >>> m.some attribute() 190 | 42 191 | 192 | Również tworząc nowy obiekt możemy podać parametr ``return_value``. Dzięki któremu, 193 | wywołanie danego mocka zpowoduje zwrócenie konkretnej wartości. 194 | 195 | .. code-block:: python 196 | 197 | >>> special_class = MySpeciallClass() 198 | >>> special_class.my_method = mock.Mock(return_value=3) 199 | >>> special_class.my_method(2, 4) 200 | 3 201 | >>> m.variable.assert_called_with(2, 4) 202 | >>> m.variable.assert_called_with(2, 4, 6) 203 | ... 204 | AssertionError: Expected call: variable(2, 4, 6) 205 | Actual call: variable(2, 4) 206 | 207 | 208 | Należy pamiętać, że przypisująć do ``return_value`` konkretną funkcję zostanie zwrócony jej 209 | obiekt a sama funkcja nie zostanie wywołana. 210 | 211 | .. code-block:: python 212 | 213 | >>> def print_answer(): 214 | ... print("42") 215 | ... 216 | >>> 217 | >>> m.some_attribute.return_value = print_answer 218 | >>> m.some_attribute() 219 | 220 | 221 | Aby zwrócić wartość funkcji musimy wykorzystać inny atrybut ``side_effect``. 222 | 223 | 224 | Atrybut ``side_effect`` 225 | ^^^^^^^^^^^^^^^^^^^^^^^ 226 | 227 | Jest atrybutem który akceptuje trzy różne wartości obiektów: 228 | * obiekty wywoływalne (callable) 229 | * obiekty iterowalne (iterable) 230 | * wyjątki (exceptions) 231 | 232 | .. code-block:: python 233 | 234 | >>> m.some_attribute.side_effect = ValueError('A custom value error') 235 | >>> m.some_attribute() 236 | Traceback (most recent call last): 237 | File "", line 1, in 238 | File "/usr/lib/python3.4/unittest/mock.py", line 902, in __call__ 239 | return _mock_self._mock_call(*args, **kwargs) 240 | File "/usr/lib/python3.4/unittest/mock.py", line 958, in _mock_call 241 | raise effect 242 | ValueError: A custom value error 243 | 244 | Podając jako wartość listę, krotkę lub obiekt podobny to przy każdym wywołaniu tej metody 245 | zostanie zwrócony kolejna wartość znajdująca się w obiekcie iterowalnym. 246 | 247 | .. code-block:: python 248 | 249 | >>> m.some_attribute.side_effect = range(2) 250 | >>> m.some_attribute() 251 | 0 252 | >>> m.some_attribute() 253 | 1 254 | >>> m.some_attribute() 255 | Traceback (most recent call last): 256 | File "", line 1, in 257 | File "/usr/lib/python3.4/unittest/mock.py", line 902, in __call__ 258 | return _mock_self._mock_call(*args, **kwargs) 259 | File "/usr/lib/python3.4/unittest/mock.py", line 961, in _mock_call 260 | result = next(effect) 261 | StopIteration 262 | 263 | Ostatnią i najważniejszą możliwością jest oczywiście wywołanie obiektu `callable``. Można 264 | również ustawić konkretną klasę, a wywołanie takiej metody spowoduje utworzenie obiektu. 265 | 266 | .. code-block:: python 267 | 268 | >>> def print_answer(): 269 | ... print("42") 270 | >>> m.some_attribute.side_effect = print_answer 271 | >>> m.some_attribute.side_effect() 272 | 42 273 | 274 | .. code-block:: python 275 | 276 | >>> m.some_attribute.side_effect = lambda x: print(x) 277 | >>> m.some_attribute.side_effect(45) 278 | 45 279 | 280 | .. code-block:: python 281 | 282 | >>> class MyObject: 283 | ... def __repr__(self): 284 | ... return ''.format(id(self)) 285 | ... def get_only_id(self): 286 | ... print(id(self)) 287 | >>> m.some_attribute.side_effect = MyObject 288 | >>> m.some_attribute() 289 | 290 | >>> m.some_attribute().get_only_id() 291 | 4622375904 292 | 293 | Tworząc nowy mock również możemy ustawić wartość ``side_effect`` dzięki której wywołanie 294 | takiego moka spowoduje np. wyrzucenie wyjątku, lub przeliczenie konkretnej wartości. 295 | 296 | .. code-block:: python 297 | 298 | >>> special_class = MySpeciallClass() 299 | >>> special_class.my_method = mock.Mock(side_effect=lambda x: x * 10) 300 | >>> special_class.my_method(10) 301 | 100 302 | >>> special_class.my_method(10, 10) 303 | TypeError: () takes 1 positional argument but 2 were given 304 | 305 | 306 | Mock vs MagicMock 307 | ^^^^^^^^^^^^^^^^^ 308 | MagicMock jest podklasą klasy Mock. 309 | 310 | .. code-block:: python 311 | 312 | class MagicMock(MagicMixin, Mock) 313 | 314 | W rezultacie MagicMock zapewnia wszystko, co zapewnia Mock oraz jak można się spodziewać potrafi nieco więcej. 315 | Zamiast myśleć o Mocku jako o uboższej wersji MagicMocka, pomyśl o MagicMock jako rozszerzonej wersji Mock. 316 | To powinno odpowiedzieć na pytanie o to, dlaczego Mock istnieje i co zapewnia Mock a co MagicMock. 317 | 318 | Jedną i najważniejszą różnicą jest fakt, że MagicMock zapewnia tworzenie "magicznych" metod 319 | pythona jeśli są one potrzebne. Poprzez magiczne metody rozumiemy wszystkie metody interfejsu 320 | zawierające podwójne podkreślenie w swojej nazwie (np. ``__init__``, ``__len__`` itd.) 321 | 322 | .. note:: 323 | 324 | https://docs.python.org/3/library/unittest.mock.html#magicmock-and-magic-method-support 325 | 326 | 327 | .. code-block:: python 328 | 329 | >>> int(Mock()) 330 | TypeError: int() argument must be a string or a number, not 'Mock' 331 | >>> int(MagicMock()) 332 | 1 333 | >>> len(Mock()) 334 | TypeError: object of type 'Mock' has no len() 335 | >>> len(MagicMock()) 336 | 0 337 | 338 | Możesz "zobaczyć" metody dodane do MagicMock, ponieważ metody te są wywoływane po raz pierwszy: 339 | 340 | 341 | .. code-block:: python 342 | 343 | >>> magic1 = MagicMock() 344 | >>> dir(magic1) 345 | ['assert_any_call', 'assert_called_once_with', ...] 346 | >>> int(magic1) 347 | 1 348 | >>> dir(magic1) 349 | ['__int__', 'assert_any_call', 'assert_called_once_with', ...] 350 | >>> len(magic1) 351 | 0 352 | >>> dir(magic1) 353 | ['__int__', '__len__', 'assert_any_call', 'assert_called_once_with', ...] 354 | 355 | Dlaczego więc nie używać MagicMock przez cały czas? Postaram się postawić inne pytanie: 356 | Czy rzeczywiście potezebujemy domyślnych implementacjami metod magicznych? 357 | Przykład? Czy wywołanie indeksu na obieknie ``mocked_object[1]`` rzeczywiście powinno 358 | zwrócić wartość zamiast błędu? Czy możesz zaakceptować wszystkie niezamierzone 359 | konsekwencje z powodu zastosowania automatycznie utworzonych metod magicznych? 360 | Jeśli odpowiedź na te pytania brzmi "tak", możesz korzystać z ``MagicMock``. 361 | W przeciwnym razie korzystaj z ``Mock``. 362 | 363 | 364 | .. code-block:: python 365 | 366 | def test_setup(): 367 | external_obj = mock.Mock() 368 | obj = myobj.MyObj(external_obj) 369 | obj.setup() 370 | external_obj.setup.assert_called_with(cache=True, max_connections=256) 371 | 372 | 373 | Specialne metody i atrybuty obiektu 374 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 375 | 376 | * `called`_ — zwraca wartość ``True`` / ``False`` pokazując czy obiekt był wywołany 377 | * `call_count`_ — zwraca wartość ilość wywołań obiektu 378 | * `call_args`_ — zwraca argumenty z ostatniego wywołania 379 | * `call_args_list`_ — zwraca listę wywołań 380 | * `method_calls`_ — zwraca ścieżkę wywołań metod i atrybutów oraz ich metod i atrybutów 381 | * `mock_calls`_ — zwraca zapis wywołań do symulowanego obiektu, jego metod, atrybutów i zwracanych wartości 382 | * `attach_mock`_ - pozwala dołączyć do obiektu nowy atrybut, metodę 383 | * `configure_mock`_ - pozwala skonfigurować wartości obiektu poprzez wykorzystanie słownika 384 | * `mock_add_spec`_ - pozwala na podstawie stringu lub obiektu ustawić wartości dla obiektu 385 | * `reset_mock`_ - resetuje wartości wywołania obiektu 386 | * `return_value`_ - zwraca jedną wartość niezależnie czy wywołamy ją jako zmienną czy metodę 387 | * `side_effect`_ - zwraca wywołanie funkcji, przekazanie list zwraca po każdym elemencie 388 | 389 | .. _`called`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.called 390 | .. _`call_count`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_count 391 | .. _`call_args`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args 392 | .. _`call_args_list`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args_list 393 | .. _`method_calls`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.method_calls 394 | .. _`mock_calls`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.mock_calls 395 | .. _`attach_mock`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.attach_mock 396 | .. _`configure_mock`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.configure_mock 397 | .. _`mock_add_spec`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.mock_add_spec 398 | .. _`reset_mock`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.reset_mock 399 | .. _`return_value`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.return_value 400 | .. _`side_effect`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.side_effect 401 | 402 | 403 | Specialne aseracje dostępne w obiekcie 404 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 405 | 406 | W testach jednostkowych powszechnie stosowane są aseracje. Aby to poprawić komfort pracy 407 | biblioteka ``mock`` zawiera wbudowane funkcje asercji, które odwołują się do wyżej 408 | wymienionych atrybutów: 409 | 410 | * `assert_called`_ - sprawdzenie czy ``Mock`` kiedykolwiek został wywołany 411 | * `assert_called_once`_ - sprawdzenie czy ``Mock`` został wywołany dokładnie jeden raz 412 | * `assert_called_with`_ - sprawdzenie konkretne argumenty użyte w ostatnim wywołaniu ``Mock`` 413 | * `assert_called_once_with`_ - sprawdzenie czy konkretne argumenty są używane dokładnie jeden raz w ``Mock`` 414 | * `assert_any_call`_ - sprawdzenie czy konkretne argumenty zostały używane w każdym wywołaniu ``Mock`` 415 | * `assert_has_calls`_ - tak samo jak ``any_call`` ale z wieloma wywołaniami ``Mock`` 416 | * `assert_not_called`_ - sprawdzenie czy ``Mock`` nigdy nie został wywołany 417 | 418 | .. _`assert_called`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called 419 | .. _`assert_called_once`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_once 420 | .. _`assert_called_with`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_with 421 | .. _`assert_called_once_with`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_once_with 422 | .. _`assert_any_call`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_any_call 423 | .. _`assert_has_calls`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls 424 | .. _`assert_not_called`: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_not_called 425 | 426 | 427 | Jak działa Patch? 428 | ----------------- 429 | 430 | Mocki można bardzo prosto wprowadzić do testów w przypadku gdy obiekty przyjmują klasy 431 | lub instancje z zewnątrz. Wystarczy utworzyć instancję klasy Mock i przekazać ją jako 432 | obiekt do systemu. Jednakże, gdy utworzony kod wykorzystuje wewnątrz inn moduły które 433 | są zaszyte w kodzie, takie proste przekazanie obiektu Mock nie zadziała. W takich 434 | przypadkiach pomaga nam `patch` obiektu. 435 | 436 | Patch oznacza zastąpienie obiektu wywoływalnego wewnątrz kodu. Dzięki temu możemy 437 | fałszować obiekty będące zaszyte w kodzie, nie modyfikując samego kodu. Patchowanie jest 438 | wykonywane w czasie wykonywania testu. 439 | 440 | W jaki sposób tworzyć patch? 441 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 442 | 443 | Domyślnie tworzymy patch nie w miejscu deklaracji funkcji lub klasy ale w miejscu gdzie 444 | została ona użyta. Poniższy przykład pokazuje tę zależność. 445 | 446 | .. code-block:: python 447 | 448 | # models.py 449 | class SpecialModel: 450 | pass 451 | 452 | .. code-block:: python 453 | 454 | #services.py 455 | from models import SpecialModel 456 | my_model = SpecialModel() 457 | 458 | .. code-block:: python 459 | 460 | @patch('services.SpecialModel') 461 | def test_patch_pony(mockspecialmodel): 462 | mockspecialmodel.return_value = 42 463 | 464 | 465 | Jednakże jeśli nie importujemy samej klasy tylko moduł ``import models`` sam sposób 466 | tworzenia patch wygląda nieco inaczej. 467 | 468 | .. code-block:: python 469 | 470 | # models.py 471 | class SpecialModel: 472 | pass 473 | 474 | .. code-block:: python 475 | 476 | #services.py 477 | import models 478 | my_model = models.SpecialModel() 479 | 480 | .. code-block:: python 481 | 482 | @patch('models.SpecialModel') 483 | def test_patch_pony(mockspecialmodel): 484 | 485 | Więcej szczegółów znajdziemy w dokumentacji https://docs.python.org/3/library/unittest.mock.html#where-to-patch. 486 | 487 | 488 | Jak działa ``autospec``? 489 | ^^^^^^^^^^^^^^^^^^^^^^^^ 490 | 491 | Efekt jego wykorzystania jest bardzo podobny do atrybutu ``spec`` podczas tworzenia Mock. 492 | Tworząc patch ``patch_A`` z klasy ``A`` będzie on miał takie same metody czy atrybuty 493 | jak klasa ``A``. Wykorzystując ``autospec`` nie można fałszować żadnych innych atrybutów, 494 | które nie są zdefiniowane w rzeczywistej klasie. 495 | 496 | ``autospec`` można wywołać na dwa sposoby: ``autospec=True`` lub ``autospec=some_object``. 497 | Podanie wartości ``True`` będzie na tworzyć Mock z dokładnymi parametrami na podstawie 498 | patchowanej klasy/funkcji. Podanie wartości konkretnego obiektu utworzy nam taki właśnie 499 | obiekt. 500 | 501 | 502 | Proste testowanie z Mock 503 | ------------------------ 504 | 505 | Celem metod dostarczanych przez pozorowane obiekty jest umożliwienie nam sprawdzenia, 506 | jakie metody wywoływaliśmy na próbce i jakie parametry wykorzystaliśmy w wywołaniu. 507 | 508 | .. note:: 509 | 510 | Według Sandy Metza musimy przetestować tylko trzy typy komunikatów (połączeń) między obiektami: 511 | 512 | * Przychodzące zapytania (asercja na wynik) 513 | * Polecenia przychodzące (asercja na bezpośrednich publicznych efektach ubocznych) 514 | * Polecenia wychodzące (oczekiwanie na połączenie i argumenty) 515 | 516 | 517 | Pierwszą rzeczą jaką chcemy przetestować jest sprawdzenie czy została wywołana jakaś metoda. 518 | Aby tego dokonać wykorzystujemy jedną z specjalnych metod. Utworzyliśmy klasę która jako 519 | argument przyjmuje obiekt który nawiazuje połączenie poprzez metodę `connect`. Również 520 | posidamy drugą metodę `setup`, która będzie ustawiać odpowiednie argumenty dla naszego obiektu. 521 | 522 | 523 | .. code-block:: python 524 | 525 | class MyObj(): 526 | def __init__(self, repo): 527 | self._repo = repo 528 | repo.connect() 529 | 530 | def setup(self): 531 | self._repo.setup(cache=True, max_connections=256) 532 | 533 | W pierwszym teście sprawdzimy czy podczas utworzenie obiektu klasy `MyObj` zostało nawiązane 534 | połączenie - czyli czy została wywołana metoda `connect`. Obiekt, który przekazujemy jest 535 | mock. Wywołując metodę `assert_called_with` sprawdzamy czy dana metoda została wywołana. 536 | 537 | .. code-block:: python 538 | 539 | def test_instantiation(): 540 | external_obj = mock.Mock() 541 | MyObj(external_obj) 542 | external_obj.connect.assert_called_with() 543 | 544 | 545 | W drugim teście sprawdzimy czy zostały przekazane odpowiednie parametry dla wywoływanej metody. 546 | Aby to sprawdzić wykorzystujemy inną metodę specialną `assert_called_with`. 547 | 548 | .. code-block:: python 549 | 550 | def test_setup(): 551 | external_obj = mock.Mock() 552 | obj = MyObj(external_obj) 553 | obj.setup() 554 | external_obj.setup.assert_called_with(cache=True, max_connections=256) 555 | 556 | 557 | Jednak nie zawsze możemy przekazać obiekty do wnętrza naszych klas, dlatego teraz 558 | spróbujemy wykorzystać ``patch`` do tego aby zastąpić pewną funkcjonalność wewnątrz 559 | naszego kodu. Poniżej zademonstruję przykład wykorzystujący wbudowaną bibliotekę ``os``. 560 | 561 | .. code-block:: python 562 | 563 | #fileinfo.py 564 | import os 565 | 566 | class FileInfo: 567 | def __init__(self, path): 568 | self.original_path = path 569 | self.filename = os.path.basename(path) 570 | 571 | def get_info(self): 572 | return self.filename, self.original_path, os.path.abspath(self.filename) 573 | 574 | Normalne wywołanie tej klasy spowoduje wyświetlenie informacji o pliku (jest to bardzo 575 | prosta klasa, w realnym świecie była by ona bezuzyteczna, służy ona jedynie aby pokazać 576 | jak działa `patch`). Inicjując powyższą klasę musimy podać nazwę pliku. Poniżej pokazano 577 | proste działanie powyższej klasy. 578 | 579 | .. code-block:: python 580 | 581 | >>> f = FileInfo('some_file.txt') 582 | >>> f.filename 583 | some_file.txt 584 | >>> f.original_path 585 | some_file.txt 586 | >>> f.get_info() 587 | ('some_file.txt', 'some_file.txt', '/home/xxx/some_file.txt') 588 | 589 | 590 | Pisząc testy będziemy chcieli sprawdzić czy czy powyżej zwracane wartości sa poprawne. Jako 591 | pierwsze sprawdzimy czy waertość ``filename`` zwraca nam poprawnie nazwę. 592 | 593 | .. code-block:: python 594 | 595 | #test_fileinfo.py 596 | from unittest.mock import patch 597 | from fileinfo import FileInfo 598 | 599 | def test_filename(): 600 | filename = 'somefile.ext' 601 | fi = FileInfo(filename) 602 | assert fi.filename == filename 603 | 604 | def test_filename_with_relative_path(): 605 | filename = 'somefile.ext' 606 | relative_path = '../{}'.format(filename) 607 | fi = FileInfo(relative_path) 608 | assert fi.filename == filename 609 | 610 | Następnie sprawdzimy czy ``original_path`` zwróci nam dokładnie taką wartość jaką podaliśmy 611 | podczas tworzenia obiektu klasy. 612 | 613 | .. code-block:: python 614 | 615 | #test_fileinfo.py 616 | from unittest.mock import patch 617 | from fileinfo import FileInfo 618 | 619 | def test_filename(): 620 | filename = 'somefile.ext' 621 | fi = FileInfo(filename) 622 | assert fi.original_path == filename 623 | 624 | def test_filename_with_relative_path(): 625 | filename = 'somefile.ext' 626 | relative_path = '../{}'.format(filename) 627 | fi = FileInfo(relative_path) 628 | assert fi.original_path == relative_path 629 | 630 | Utworzyliśmy jednak dodatkową metodę ``get_info``, która zwraca nam krotkę z dwoma powyżej 631 | przetestowanymi wartościami oraz śieżkę absolutną do pliku. I tutaj jest mały problem. 632 | Uruchamiając testy na różnych komuoterach prawdopodobnie absolutna ścieżka do pliku będzie 633 | różna (zależna od miejsca gdzie został uruchomiony projekt). Aby móc przetestować tę część 634 | kodu musimy posłużyć się ``patch``. Poniżej został pokazany kod w jaki sposób utworzyć łatkę 635 | na moduł ``os.path.abspath`` z wukorzystaniem kontekst menadżera. 636 | 637 | .. code-block:: python 638 | 639 | def test_get_info(): 640 | filename = 'somefile.ext' 641 | original_path = '../{}'.format(filename) 642 | 643 | with mock.patch('os.path.abspath') as abspath_mock: 644 | test_abspath = 'some/abs/path' 645 | abspath_mock.return_value = test_abspath 646 | fi = FileInfo(original_path) 647 | assert fi.get_info() == (filename, original_path, test_abspath) 648 | 649 | Zamiast korzystać z kontekstu menadżera możemy wykorzystać dekorator. W takim przypadku 650 | dodajemy jedną zmienną to funkcji testującej, która zwróci nam Mock obiektu ``abspath_mock``. 651 | 652 | .. code-block:: python 653 | 654 | @mock.patch('os.path.abspath') 655 | def test_get_info(abspath_mock): 656 | filename = 'somefile.ext' 657 | original_path = '../{}'.format(filename) 658 | 659 | test_abspath = 'some/abs/path' 660 | abspath_mock.return_value = test_abspath 661 | fi = FileInfo(original_path) 662 | assert fi.get_info() == (filename, original_path, test_abspath) 663 | 664 | 665 | Wykorzystanie kilku ``patch`` 666 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 667 | 668 | Wykorzystując nasz wcześniejszy przykad, możemy dodać do naszej klasy zwrócenie wielkości 669 | pliku. Aby przetestować takie zadanie musimy wykorzystać dwa patch w jednym teście. 670 | Poniższy przykład pokazuje jak to zrobić. 671 | 672 | .. code-block:: python 673 | 674 | @patch('os.path.getsize') 675 | @patch('os.path.abspath') 676 | def test_get_info(abspath_mock, getsize_mock): 677 | filename = 'somefile.ext' 678 | original_path = '../{}'.format(filename) 679 | 680 | test_abspath = 'some/abs/path' 681 | abspath_mock.return_value = test_abspath 682 | 683 | test_size = 1234 684 | getsize_mock.return_value = test_size 685 | 686 | fi = FileInfo(original_path) 687 | assert fi.get_info() == (filename, original_path, test_abspath, test_size) 688 | 689 | 690 | Należy jednak pamiętać o kolejności argumentów w funkcji testujacej. Pierwszy argument funkcji 691 | jest wartoscią zwracaną przez dekorator znajdujacy się najbliżej funkcji. Dlaczego tak jest? 692 | Poniższy przedstawiono funkcję która została obudowana dwoma dekoratorami. 693 | 694 | .. code-block:: python 695 | 696 | @decorator1 697 | @decorator2 698 | def myfunction(): 699 | pass 700 | 701 | 702 | W rzeczywistości ten sam efekt można uzyskać wywołując jawnie dwie funkcję które w swoich 703 | argumentach będą zawierać kolejne funkcje. 704 | 705 | .. code-block:: python 706 | 707 | def myfunction(): 708 | pass 709 | myfunction = decorator1(decorator2(myfunction)) 710 | 711 | 712 | Dlatego kolejność argumentów jest ważna i w powyższym przykładzie będzie ona następująca. 713 | 714 | .. code-block:: python 715 | 716 | @decorator1 717 | @decorator2 718 | def myfunction(args_decorator2, args_decorator1): 719 | pass 720 | 721 | 722 | Tworzenie ``patch`` dla obiektów niemutowalnych 723 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 724 | 725 | Należy pamiętać, że tymczasowe zastąpienie obiektu który jest niezmienny, operacja 726 | tworzenia ``patch`` nie powiedzie się. Typowym przykładem tego problemu jest moduł 727 | ``datetime``, który jest również jednym z najlepszych kandydatów do tworzenia ``patch``, 728 | ponieważ wyjście funkcji czasu jest z definicji zmienne w czasie. Poniższy przykład 729 | pokazuje proste zastosowanie powyższego problemu. 730 | 731 | .. code-block:: python 732 | 733 | #logger.py 734 | import datetime 735 | 736 | class Logger: 737 | def __init__(self): 738 | self.messages = [] 739 | 740 | def log(self, message): 741 | self.messages.append((datetime.datetime.now(), message)) 742 | 743 | Jeśli spróbujemy napisać ``patch`` dla modułu ``datetime.datetime.now``, możemy być bardzo zaskoczeni. 744 | 745 | .. code-block:: python 746 | 747 | from unittest.mock import patch 748 | from logger import Logger 749 | 750 | def test_init(): 751 | l = Logger() 752 | assert l.messages == [] 753 | 754 | @patch('datetime.datetime.now') 755 | def test_log(mock_now): 756 | test_now = 123 757 | test_message = "A test message" 758 | mock_now.return_value = test_now 759 | 760 | l = Logger() 761 | l.log(test_message) 762 | assert l.messages == [(test_now, test_message)] 763 | 764 | Uruchamiając powyższy test, otrzymamy błąd informujący na o braku możliwości 765 | ustawienia atrybutu ``datetime.datetime``. 766 | 767 | .. code-block:: python 768 | 769 | TypeError: can't set attributes of built-in/extension type 'datetime.datetime' 770 | 771 | 772 | Istnieje kilka sposobów rozwiązania tego problemu, ale wszystkie z nich wykorzystują fakt, 773 | że podczas importowania podklasy niezmiennego obiektu, tworzona jest jego "kopia" która 774 | umożliwia nam utworzenia ``patch``. 775 | 776 | W pierwszym teście staramy się tworzyć ``patch`` bezpośrednio na obiekcie ``datetime.datetime.now``, 777 | prubując wpływająć na wbudowany moduł ``datetime``. Plik logger.py jednak importuje moduł ``datetime``, 778 | dzięki czemu staje się on lokalnym symbolem w module ``logger``. Ta cecha jest klucz do 779 | rozwiązania naszego problemu i utworzenia ``patch``. 780 | 781 | .. code-block:: python 782 | 783 | @patch('logger.datetime.datetime') 784 | def test_log(mock_datetime): 785 | test_now = 123 786 | test_message = "A test message" 787 | mock_datetime.now.return_value = test_now 788 | 789 | l = Logger() 790 | l.log(test_message) 791 | assert l.messages == [(test_now, test_message)] 792 | 793 | W tym teście zmieniliśmy dwie rzeczy. Najpierw łatamy moduł zaimportowany do pliku 794 | ``logger.py``, a nie moduł dostarczany globalnie przez interpreter Pythona. Po drugie, 795 | musimy załatać cały moduł, ponieważ jest to plik importowany przez ``logger.py``. 796 | 797 | Próbując utworzyć ``patch`` dla całego modułu ``logger.datetime.datetime.now`` również 798 | otrzymay komunikat z błędem, poniważ obiekt jest on wciąż niezmienny. 799 | 800 | Innym możliwym rozwiązaniem tego problemu jest utworzenie funkcji, która wywołuje 801 | niezmienny obiekt i zwraca jego wartość. 802 | 803 | 804 | Wykorzystanie pytest-mock 805 | ------------------------- 806 | 807 | Już wiemy jak działa obiekt ``Mock``, ``MagicMock`` czy ``patch``. Korzystając z dodatku 808 | ``pytest-mock`` mamy możliwość w jeszcze prostszy sposób używania tych właśnie funkcji. 809 | Nie musimy korzystać z dekoratora i zastanawiać się która wartość jest pierwsza. Jedyne 810 | co robimy to wykrzystujemy fixture ``mocker``. 811 | 812 | .. code-block:: python 813 | 814 | import os 815 | 816 | class UnixFS: 817 | @staticmethod 818 | def rm(filename): 819 | os.remove(filename) 820 | 821 | def test_unix_fs(mocker): 822 | mocker.patch('os.remove') 823 | UnixFS.rm('file') 824 | os.remove.assert_called_once_with('file') 825 | 826 | 827 | .. code-block:: python 828 | 829 | def test_foo(mocker): 830 | mocker.patch('os.remove') 831 | mocker.patch.object(os, 'listdir', autospec=True) 832 | mocked_isfile = mocker.patch('os.path.isfile') 833 | 834 | def test_create_mock(mocker): 835 | request.user = mocker.Mock(User) 836 | 837 | 838 | .. code-block:: python 839 | 840 | def get_example(): 841 | r = requests.get('http://example.com/') 842 | return r.status_code == 200 843 | 844 | def test_get_example_passing(mocker): 845 | mocked_get = mocker.patch('requests.get', autospec=True) 846 | mocked_req_obj = mock.Mock() 847 | mocked_req_obj.status_code = 200 848 | mocked_get.return_value = mocked_req_obj 849 | assert(get_example()) 850 | 851 | mocked_get.assert_called() 852 | mocked_get.assert_called_with('http://example.com/') 853 | 854 | .. note:: 855 | 856 | ``pytest-mock`` wspiera następujące metody: 857 | 858 | * mocker.patch 859 | * mocker.patch.object 860 | * mocker.patch.multiple 861 | * mocker.patch.dict 862 | * mocker.stopall() 863 | * mocker.resetall() 864 | 865 | 866 | .. note:: 867 | 868 | Niektóre obiekty z modułu ``mock`` są dostępne bezpośrednio z ``mocker``. 869 | 870 | * mocker.Mock 871 | * mocker.MagicMock 872 | * mocker.PropertyMock 873 | * mocker.ANY 874 | * mocker.DEFAULT 875 | * mocker.call 876 | * mocker.sentinel 877 | * mocker.mock_open 878 | -------------------------------------------------------------------------------- /page/pytest/pytest_factoryboy.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Pytest FactoryBoy 3 | ================= 4 | 5 | `Factory Boy`_ jest narzędziem tworzącym fabryki dla obiektów, co oznacza, że nie musimy ręcznie 6 | tworzyć potrzebnych obiektów do testów, ale możemy je wygenerować od razu w podanej ilości 7 | w bardzo prosty sposób. Możemy ustawiać na tworzonych obiektach własne wartości tylko dla 8 | zmiennych które chcemy przetestować. 9 | 10 | `pytest-factoryboy`_ jest dodatkiem do ``pytest`` pozwalającym na wykorzystanie fabryk jako 11 | fixture. Pozwala również na wykorzystanie markera ``parametrize`` do parametryzowania tworzonych fabryk. 12 | Dzięki takiemu rozwiązaniu nie musimy importować do naszych testów fabryk a wykorzystując 13 | odpowiednią składnię możemy importować gotowe obiekty utworzone w bazie danych. 14 | 15 | Instalacja 16 | ---------- 17 | 18 | .. code-block:: bash 19 | 20 | $ pip install pytest-factoryboy 21 | 22 | 23 | 24 | .. attention:: 25 | 26 | Warto zainstalować najnowszą wersję 2.0.1. Obecnie tylko ta wersja zapewnia wsparcie dla najnowszej wersji factoryboy 2.10.0 27 | 28 | 29 | Daklarowanie i tworzenie fabryk 30 | ------------------------------- 31 | 32 | Tworzenie fabryki polega na utworzeniu klasy która odzwierciedla pola klasy dla której tworzymy 33 | fabrykę. Ważne jest aby w klasie ``Meta`` w atrybucie ``model`` zdefiniować dla jakiego 34 | modelu budujemy fabrykę. 35 | 36 | 37 | .. code-block:: python 38 | 39 | import factory 40 | from . import models 41 | 42 | class UserFactory(factory.Factory): 43 | first_name = 'John' 44 | last_name = 'Doe' 45 | admin = False 46 | 47 | class Meta: 48 | model = models.User 49 | 50 | # Another, different, factory for the same object 51 | class AdminFactory(factory.Factory): 52 | first_name = 'Admin' 53 | last_name = 'User' 54 | admin = True 55 | 56 | class Meta: 57 | model = 'user.User' 58 | 59 | 60 | Nową fabryję możemy utworzyć na 3 sposoby: 61 | 62 | .. code-block:: python 63 | 64 | # Zwrócenie instancji, która nie jest zapisana 65 | user = UserFactory.build() 66 | 67 | # Zwrócenie instancji, która została zapisana 68 | user = UserFactory.create() 69 | 70 | # Zwrócenie obiektu stub (tylko kilka atrybutów) 71 | obj = UserFactory.stub() 72 | 73 | Możemy również utworzyć kilka obiektów: 74 | 75 | .. code-block:: python 76 | 77 | # Zwrócenie 10 instancji, które nie są zapisane 78 | users = UserFactory.build_batch(10, first_name="Joe") 79 | 80 | # Zwrócenie 10 instancji które zostały zapisane w bazie danych 81 | users = UserFactory.create_batch(10, first_name="Joe") 82 | 83 | # Zwrócenie 10 obiektu stub posiadających tylko kilka atrybutów 84 | users = UserFactory.stub_batch(10, first_name="Joe") 85 | 86 | 87 | Typy tworzonych pól 88 | ------------------- 89 | 90 | FactoryBoy zawiera duża liczbę typów pól dzięki którym możemy przygotować fabrykę która, 91 | w odpowiedni sposób będzie generować obiekty. 92 | 93 | Faker 94 | ^^^^^ 95 | 96 | Aby łatwo zdefiniować realistycznie wyglądające fabryki, najczęściej wykorzystywany zostaje atrybutu Faker. 97 | Działanie tego atrybutu jest bardzo proste, jako pierwszy argument podajemy funkcję modułu 98 | Faker http://faker.readthedocs.io/en/master/providers.html 99 | 100 | Przykładowo z modułu ``faker.providers.person`` wybieramy funkcję ``name``. 101 | Jako dodatkowy argument możemy podać język w jakim ma zostać utworzony atrybut. 102 | 103 | .. code-block:: python 104 | 105 | class UserFactory(factory.Factory): 106 | class Meta: 107 | model = models.User 108 | 109 | username = factory.Faker('name', locale='pl_PL') 110 | 111 | Z modułu ``faker.providers.lorem`` wybierając funckję ``paragraph`` możemy jako argument 112 | przekazać dodatkowe parametry. 113 | 114 | .. code-block:: python 115 | 116 | class UserFactory(factory.Factory): 117 | class Meta: 118 | model = models.User 119 | 120 | about_me = factory.Faker('paragraph', nb_sentences=3, variable_nb_sentences=True, locale='pl_PL') 121 | 122 | 123 | Słownik 124 | ^^^^^^^ 125 | 126 | Jeśli nasze pole oczekuje słownika możemy je utworzyć w poniższy sposób. Chcąc odwołać się 127 | do atrybutów obiektu musimy wpisać ``..is_superuser``. 128 | 129 | .. code-block:: python 130 | 131 | class UserFactory(factory.Factory): 132 | class Meta: 133 | model = User 134 | 135 | is_superuser = False 136 | roles = factory.Dict({ 137 | 'role1': True, 138 | 'role2': False, 139 | 'role3': factory.Iterator([True, False]), 140 | 'admin': factory.SelfAttribute('..is_superuser'), 141 | }) 142 | 143 | 144 | Lista 145 | ^^^^^ 146 | 147 | Możemy również utworzyć listę. Wewnętrznie, pola są konwertowane na `indeks=wartość`, 148 | co umożliwia zastąpienie niektórych wartości w czasie ich użycia. 149 | 150 | .. code-block:: python 151 | 152 | class UserFactory(factory.Factory): 153 | class Meta: 154 | model = User 155 | 156 | flags = factory.List([ 157 | 'user', 158 | 'active', 159 | 'admin', 160 | ]) 161 | 162 | .. code-block:: python 163 | 164 | >>> u = UserFactory(flags__2='superadmin') 165 | >>> u.flags 166 | ['user', 'active', 'superadmin'] 167 | 168 | 169 | Sekwencje 170 | ^^^^^^^^^ 171 | 172 | Jeśli pole ma posiadać unikalny klucz, każdy obiekt generowany przez fabrykę powinien 173 | mieć inną wartość dla tego pola. Aby osiągnąć taki efekt wykorzystujemy deklarację sekwencji: 174 | 175 | .. code-block:: python 176 | 177 | class UserFactory(factory.Factory): 178 | class Meta: 179 | model = models.User 180 | 181 | username = factory.Sequence(lambda n: 'user%d' % n) 182 | 183 | Jeśli jes ona bardziej skomplikowana można ją również zapisać w poniższy sposób. 184 | 185 | .. code-block:: python 186 | 187 | class UserFactory(factory.Factory): 188 | class Meta: 189 | model = models.User 190 | 191 | @factory.sequence 192 | def username(n): 193 | return 'user%d' % n 194 | 195 | Każde wywołanie obiektu wygeneruje nam nowy niepowtarzalny atrybut. 196 | 197 | .. code-block:: python 198 | 199 | >>> UserFactory() 200 | 201 | >>> UserFactory() 202 | 203 | 204 | 205 | Maybe 206 | ^^^^^ 207 | 208 | Czasami sposób budowania danego pola może zależeć od wartości innego, na przykład parametru. 209 | W takich przypadkach można użyj deklaracji ``Maybe``: przyjmuje nazwę pola "decydującego" oraz dwie deklaracje. 210 | w zależności od wartości pola, którego nazwa jest przechowywana w parametrze "decydującym", 211 | zastosuje efekty jednej lub drugiej deklaracji. 212 | 213 | .. code-block:: python 214 | 215 | class UserFactory(factory.Factory): 216 | class Meta: 217 | model = User 218 | 219 | is_active = True 220 | deactivation_date = factory.Maybe( 221 | 'is_active', 222 | yes_declaration=None, 223 | no_declaration=factory.fuzzy.FuzzyDateTime(timezone.now() - datetime.timedelta(days=10)), 224 | ) 225 | 226 | .. code-block:: python 227 | 228 | >>> u = UserFactory(is_active=True) 229 | >>> u.deactivation_date 230 | None 231 | >>> u = UserFactory(is_active=False) 232 | >>> u.deactivation_date 233 | datetime.datetime(2017, 4, 1, 23, 21, 23, tzinfo=UTC) 234 | 235 | 236 | LazyFunction 237 | ^^^^^^^^^^^^ 238 | 239 | W prostych przypadkach wywołanie funkcji wystarcza aby utworzyć wartości dla pól. 240 | Jeśli ta funkcja nie zależy od budowanego obiektu, najlepiej użyć LazyFunction, aby 241 | wywołać tę funkcję. LazyFunction otrzymuje funkcję, która nie przyjmuje żadnych argumentów. 242 | 243 | .. code-block:: python 244 | 245 | class LogFactory(factory.Factory): 246 | class Meta: 247 | model = models.Log 248 | 249 | timestamp = factory.LazyFunction(datetime.now) 250 | 251 | .. code-block:: python 252 | 253 | >>> LogFactory() 254 | 255 | 256 | >>> # The LazyFunction can be overriden 257 | >>> LogFactory(timestamp=now - timedelta(days=1)) 258 | 259 | 260 | 261 | LazyAttribute 262 | ^^^^^^^^^^^^^ 263 | 264 | Gdy mamy sytuację w której nasze pole jest zależne od innych najlepiej wykorzystać LazyAttribute. 265 | Dobrym przykładem może być generowanie adresu e-mail w oparciu o nazwię użytkownika. 266 | 267 | .. code-block:: python 268 | 269 | class UserFactory(factory.Factory): 270 | class Meta: 271 | model = models.User 272 | 273 | username = factory.Sequence(lambda n: 'user%d' % n) 274 | email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username) 275 | 276 | Jeśli posiadamy bardziej rozbudowaną logikę możemy wykorzystać dekorator 277 | 278 | .. code-block:: python 279 | 280 | class UserFactory(factory.Factory): 281 | class Meta: 282 | model = models.User 283 | 284 | username = factory.Sequence(lambda n: 'user%d' % n) 285 | 286 | @factory.lazy_attribute 287 | def email(self): 288 | return '%s@example.com' % self.username 289 | 290 | .. code-block:: python 291 | 292 | >>> UserFactory() 293 | 294 | 295 | >>> # The LazyAttribute handles overridden fields 296 | >>> UserFactory(username='john') 297 | 298 | 299 | >>> # They can be directly overridden as well 300 | >>> UserFactory(email='doe@example.com') 301 | 302 | 303 | 304 | FileField 305 | ^^^^^^^^^ 306 | 307 | Specialnie dla modelu Django został przygotowany atrybut ``factory.django.FileField``. 308 | Pozwala on na utworzenie pliku dla generowanej fabryki. 309 | 310 | .. code-block:: python 311 | 312 | class MyFactory(factory.django.DjangoModelFactory): 313 | class Meta: 314 | model = models.MyModel 315 | 316 | the_file = factory.django.FileField(filename='the_file.dat') 317 | 318 | .. code-block:: python 319 | 320 | >>> MyFactory(the_file__data=b'uhuh').the_file.read() 321 | b'uhuh' 322 | >>> MyFactory(the_file=None).the_file 323 | None 324 | 325 | 326 | ImageField 327 | ^^^^^^^^^^ 328 | 329 | Istnieje również atrybut ``django.db.models.ImageField`` pozwalający na tworzenie obrazków. 330 | 331 | .. code-block:: python 332 | 333 | class MyFactory(factory.django.DjangoModelFactory): 334 | class Meta: 335 | model = models.MyModel 336 | 337 | the_image = factory.django.ImageField(color='blue') 338 | 339 | .. code-block:: python 340 | 341 | >>> MyFactory(the_image__width=42).the_image.width 342 | 42 343 | >>> MyFactory(the_image=None).the_image 344 | None 345 | 346 | 347 | Non-kwarg arguments 348 | ^^^^^^^^^^^^^^^^^^^ 349 | 350 | Niektóre klasy pobierają najpierw kilka `non-kwarg` argumentów. 351 | Taki typ pola można obsłużyć za pomocą atrybutu inline_args. 352 | 353 | .. code-block:: python 354 | 355 | class MyFactory(factory.Factory): 356 | class Meta: 357 | model = MyClass 358 | inline_args = ('x', 'y') 359 | 360 | x = 1 361 | y = 2 362 | z = 3 363 | 364 | .. code-block:: python 365 | 366 | >>> MyFactory(y=4) 367 | 368 | 369 | 370 | Parametry 371 | ^^^^^^^^^ 372 | 373 | Jeśli tworzone pole jest zależne od atrybutu nie będącego polem w rzeczywistym modelu 374 | tworzonym przez fabrykę należy wykorzystać deklarację Paramtru. 375 | 376 | .. code-block:: python 377 | 378 | class RentalFactory(factory.Factory): 379 | class Meta: 380 | model = Rental 381 | 382 | begin = factory.fuzzy.FuzzyDate(start_date=datetime.date(2000, 1, 1)) 383 | end = factory.LazyAttribute(lambda o: o.begin + o.duration) 384 | 385 | class Params: 386 | duration = 12 387 | 388 | 389 | .. code-block:: python 390 | 391 | >>> RentalFactory(duration=0) 392 | 2012-03-03> 393 | >>> RentalFactory(duration=10) 394 | 2012-12-26> 395 | 396 | 397 | Cechy 398 | ^^^^^ 399 | 400 | Jeśli natomiast wiele pól ma zostać zaktualizowanych na podstawie flagi należy 401 | wykorzystać deklarację Cechy. 402 | 403 | .. code-block:: python 404 | 405 | class OrderFactory(factory.Factory): 406 | status = 'pending' 407 | shipped_by = None 408 | shipped_on = None 409 | 410 | class Meta: 411 | model = Order 412 | 413 | class Params: 414 | shipped = factory.Trait( 415 | status='shipped', 416 | shipped_by=factory.SubFactory(EmployeeFactory), 417 | shipped_on=factory.LazyFunction(datetime.date.today), 418 | ) 419 | 420 | .. code-block:: python 421 | 422 | >>> OrderFactory() 423 | 424 | >>> OrderFactory(shipped=True) 425 | 426 | 427 | 428 | Fabryki w Django 429 | ---------------- 430 | 431 | Wszystkie fabryki modelu ``Django`` powinny używać klasy bazowej ``DjangoModelFactory``. 432 | Jeśli zachodzi potrzeba utworzenia całkiem nie standardowej fabryki warto skorzystać z 433 | dokumentacji FactoryBoy https://factoryboy.readthedocs.io/en/latest/recipes.html 434 | 435 | 436 | Deklarowanie fabryk 437 | ^^^^^^^^^^^^^^^^^^^ 438 | 439 | Deklaracja przebiega w dokładnie taki sam sposób jak tworzenie fabryki z prostej klasy. 440 | Dziedzicząc jednak z DjangoModelFactory otzymujemy do ustawień 2 dodatkowe paramtery. 441 | ``django_get_or_create`` oraz ``database``. Pierwszy z nich pokreśla w jaki sposób mają 442 | zostać tworzone obiekty a drugi określa jakie bazy danych chcemu używać. 443 | 444 | .. code-block:: python 445 | 446 | class UserFactory(factory.django.DjangoModelFactory): 447 | class Meta: 448 | model = 'myapp.User' # Equivalent to ``model = myapp.models.User`` 449 | django_get_or_create = ('username',) 450 | 451 | username = 'john' 452 | 453 | 454 | .. code-block:: python 455 | 456 | >>> UserFactory() # Creates a new user 457 | 458 | >>> User.objects.all() 459 | [] 460 | 461 | >>> UserFactory() # Fetches the existing user 462 | 463 | >>> User.objects.all() # No new user! 464 | [] 465 | 466 | >>> UserFactory(username='jack') # Creates another user 467 | 468 | >>> User.objects.all() 469 | [, ] 470 | 471 | 472 | Strategie tworzenia 473 | ^^^^^^^^^^^^^^^^^^^ 474 | 475 | Tworząc obiekt posiadamy tylko dwie podstawowe strategie określające w jaki sposób ma 476 | on zostać utworzony obiekt podczas wywołania fabryki. Pierwsza z nich ``build`` tworzy 477 | obiekt lokalnie, natomiast druga ``create`` tworzy lokalny obiekt i zapisuje go 478 | w bazie danych. 479 | 480 | Domyślną strategią wywołania fabryki jest ``create``, można jednak to zmienić 481 | ustawiając atrybut strategii Meta klasy. 482 | 483 | Podstawowe strategie to ``factory.BUILD_STRATEGY`` oraz ``factory.CREATE_STRATEGY``. 484 | 485 | .. code-block:: python 486 | 487 | class ImageFactory(factory.Factory): 488 | # The model expects "attributes" 489 | form_attributes = ['thumbnail', 'black-and-white'] 490 | 491 | class Meta: 492 | model = Image 493 | strategy = factory.BUILD_STRATEGY 494 | 495 | 496 | Dziedziczenie fabryk 497 | ^^^^^^^^^^^^^^^^^^^^ 498 | 499 | Po zdefiniowaniu "bazowej" fabryki dla danej klasy, alternatywne wersje mogą być łatwo zdefiniowane poprzez podklasę. 500 | Podklasowana Fabryka dziedziczy wszystkie deklaracje od rodzica i aktualizuje je własnymi deklaracjami. 501 | 502 | .. code-block:: python 503 | 504 | class UserFactory(factory.Factory): 505 | class Meta: 506 | model = base.User 507 | 508 | firstname = "John" 509 | lastname = "Doe" 510 | group = 'users' 511 | 512 | class AdminFactory(UserFactory): 513 | admin = True 514 | group = 'admins' 515 | 516 | 517 | .. code-block:: python 518 | 519 | >>> user = UserFactory() 520 | >>> user 521 | 522 | >>> user.group 523 | 'users' 524 | 525 | >>> admin = AdminFactory() 526 | >>> admin 527 | 528 | >>> admin.group # The AdminFactory field has overridden the base field 529 | 'admins' 530 | 531 | 532 | Pole ForeignKey 533 | ^^^^^^^^^^^^^^^ 534 | 535 | Jeśli atrybut jest złożonym polem (np. ForeignKey do innego modelu), należy użyć deklaracji SubFactory. 536 | 537 | .. code-block:: python 538 | 539 | # models.py 540 | class User(models.Model): 541 | first_name = models.CharField() 542 | group = models.ForeignKey(Group) 543 | 544 | 545 | # factories.py 546 | import factory 547 | from . import models 548 | 549 | class UserFactory(factory.django.DjangoModelFactory): 550 | class Meta: 551 | model = models.User 552 | 553 | first_name = factory.Sequence(lambda n: "Agent %03d" % n) 554 | group = factory.SubFactory(GroupFactory) 555 | 556 | 557 | Jeśli wartości klucza ForeignKey muszą zostać wybrane z już wypełnionej tabeli 558 | (np. ``django.contrib.contenttypes.models.ContentType``), należy użyć ``fabryki.Iterator``. 559 | 560 | .. code-block:: python 561 | 562 | import factory, factory.django 563 | from . import models 564 | 565 | class UserFactory(factory.django.DjangoModelFactory): 566 | class Meta: 567 | model = models.User 568 | 569 | language = factory.Iterator(models.Language.objects.all()) 570 | 571 | 572 | Odwrotne relacje ForeignKey 573 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 574 | 575 | Jeśli obiekt powiązany powinien zostać utworzony podczas tworzenia obiektu 576 | (np. odwrócona relacja ForeignKey z innego Modelu), należy użyć deklaracji ``RelatedFactory``. 577 | 578 | .. code-block:: python 579 | 580 | # models.py 581 | class User(models.Model): 582 | pass 583 | 584 | class UserLog(models.Model): 585 | user = models.ForeignKey(User) 586 | action = models.CharField() 587 | 588 | 589 | # factories.py 590 | class UserFactory(factory.django.DjangoModelFactory): 591 | class Meta: 592 | model = models.User 593 | 594 | log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) 595 | 596 | 597 | Po utworzeniu instancji `UserFactory`, pole `factory_boy` wywoła 598 | ``UserLogFactory(user=that_user, action=...)`` tuż przed zwróceniem utworzonego użytkownika. 599 | 600 | 601 | Pole ManyToMany 602 | ^^^^^^^^^^^^^^^ 603 | 604 | Zbudowanie odpowiedniego połączenia między dwoma modelami zależy w dużej mierze od 605 | przypadku użycia. `factory_boy` niestety nie zapewnia narzędzia działającego w podobniy 606 | sposób jak w przypadku `SubFactory` lub `RelatedFactory`, dlatego programista musi 607 | tworzyć własne zależności od modelu. Aby utworzyć relację M2M należy wykorzystać hook 608 | ``post_generation``. 609 | 610 | .. code-block:: python 611 | 612 | # models.py 613 | class Group(models.Model): 614 | name = models.CharField() 615 | 616 | class User(models.Model): 617 | name = models.CharField() 618 | groups = models.ManyToManyField(Group) 619 | 620 | 621 | # factories.py 622 | class GroupFactory(factory.django.DjangoModelFactory): 623 | class Meta: 624 | model = models.Group 625 | 626 | name = factory.Sequence(lambda n: "Group #%s" % n) 627 | 628 | class UserFactory(factory.django.DjangoModelFactory): 629 | class Meta: 630 | model = models.User 631 | 632 | name = "John Doe" 633 | 634 | @factory.post_generation 635 | def groups(self, create, extracted, **kwargs): 636 | if not create: 637 | # Simple build, do nothing. 638 | return 639 | 640 | if extracted: 641 | # A list of groups were passed in, use them 642 | for group in extracted: 643 | self.groups.add(group) 644 | 645 | 646 | Podczas wywoływania funkcji ``UserFactory()`` lub ``UserFactory.build()`` nie zostanie 647 | utworzone powiązanie z grupą. Natomiast po wywołaniu ``UserFactory.create(groups=(group1, group2, group3))`` 648 | deklaracja ``groups`` doda przekazane grupy do użytkownika. 649 | 650 | .. code-block:: python 651 | 652 | class ClinicFactory(factory.django.DjangoModelFactory): 653 | name = 'Some name' 654 | 655 | street = factory.Faker('street_name') 656 | postal_code = factory.Faker('postcode') 657 | place = factory.Faker('city') 658 | voivodship = factory.Faker('region') 659 | country = 'Polska' 660 | 661 | @factory.post_generation 662 | def domains(self, create, data=None, **kwargs): 663 | if not create: 664 | return 665 | 666 | if data is None: 667 | data = 1 668 | 669 | if isinstance(data, int): 670 | domain_factory = getattr(DomainFactory, 'create') 671 | for i in range(data): 672 | self.domains.add(domain_factory()) 673 | elif data: 674 | for domain in data: 675 | self.domains.add(domain) 676 | 677 | class Meta: 678 | model = 'clinics.Clinic' 679 | 680 | Innnym przykładem jest możliwość utworzenia deklaracji która będzie przyjmowała liczbę lub 681 | obiekt iterowalny aby utworzyć obiekty powiązane. Nie podając żadnej wartości zostanie 682 | utworzony i dołączony 1 obiekt ``DomainFactory``. 683 | 684 | 685 | Pole ManyToMany (through) 686 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 687 | 688 | Aby utworzyć relację Many2Many poprzez własną tabelę (throw) należy wykorzystać 689 | deklarację ``RelatedFactory``. 690 | 691 | .. code-block:: python 692 | 693 | # models.py 694 | class User(models.Model): 695 | name = models.CharField() 696 | 697 | class Group(models.Model): 698 | name = models.CharField() 699 | members = models.ManyToManyField(User, through='GroupLevel') 700 | 701 | class GroupLevel(models.Model): 702 | user = models.ForeignKey(User) 703 | group = models.ForeignKey(Group) 704 | rank = models.IntegerField() 705 | 706 | 707 | # factories.py 708 | class UserFactory(factory.django.DjangoModelFactory): 709 | class Meta: 710 | model = models.User 711 | 712 | name = "John Doe" 713 | 714 | class GroupFactory(factory.django.DjangoModelFactory): 715 | class Meta: 716 | model = models.Group 717 | 718 | name = "Admins" 719 | 720 | class GroupLevelFactory(factory.django.DjangoModelFactory): 721 | class Meta: 722 | model = models.GroupLevel 723 | 724 | user = factory.SubFactory(UserFactory) 725 | group = factory.SubFactory(GroupFactory) 726 | rank = 1 727 | 728 | class UserWithGroupFactory(UserFactory): 729 | membership = factory.RelatedFactory(GroupLevelFactory, 'user') 730 | 731 | class UserWith2GroupsFactory(UserFactory): 732 | membership1 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group1') 733 | membership2 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group2') 734 | 735 | 736 | Niestandardowa metoda tworząca fabrykę 737 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 738 | 739 | Czasami zachodzi potrzeba aby tworząc fabrykę zachowywała się ona inaczej niż domyślna 740 | metoda Model.objects.create(). Aby uzyskać żądane zachowanie należy utworzyć własną metodę 741 | klasy ``_create(...)``. 742 | 743 | .. code-block:: python 744 | 745 | class UserFactory(factory.DjangoModelFactory): 746 | class Meta: 747 | model = UserenaSignup 748 | 749 | username = "l7d8s" 750 | email = "my_name@example.com" 751 | password = "my_password" 752 | 753 | @classmethod 754 | def _create(cls, model_class, *args, **kwargs): 755 | """Override the default ``_create`` with our custom call.""" 756 | manager = cls._get_manager(model_class) 757 | # The default would use ``manager.create(*args, **kwargs)`` 758 | return manager.create_user(*args, **kwargs) 759 | 760 | 761 | Wyłaczanie sygnałów 762 | ^^^^^^^^^^^^^^^^^^^ 763 | 764 | .. code-block:: python 765 | 766 | # foo/factories.py 767 | 768 | import factory 769 | import factory.django 770 | 771 | from . import models 772 | from . import signals 773 | 774 | @factory.django.mute_signals(signals.pre_save, signals.post_save) 775 | class FooFactory(factory.django.DjangoModelFactory): 776 | class Meta: 777 | model = models.Foo 778 | 779 | 780 | .. code-block:: python 781 | 782 | def make_chain(): 783 | with factory.django.mute_signals(signals.pre_save, signals.post_save): 784 | # pre_save/post_save won't be called here. 785 | return SomeFactory(), SomeOtherFactory() 786 | 787 | 788 | Konwertowanie fabryki do słownika 789 | --------------------------------- 790 | 791 | .. code-block:: python 792 | 793 | class UserFactory(factory.django.DjangoModelFactory): 794 | class Meta: 795 | model = models.User 796 | 797 | first_name = factory.Sequence(lambda n: "Agent %03d" % n) 798 | username = factory.Faker('username') 799 | 800 | .. code-block:: python 801 | 802 | >>> factory.build(dict, FACTORY_CLASS=UserFactory) 803 | {'first_name': "Agent 001", 'username': 'john_doe'} 804 | 805 | 806 | Inicjalizacja fabryk w pytest 807 | ----------------------------- 808 | 809 | Funkcje dostarczane wraz z pytest-factoryboy pozwalają na używanie fabryk bez ich importowania. 810 | Konwencja wykorzystywana do uruchamiania fixture z zarejestrowanej klasy wykorzystuj podkreślenia i małe litery. 811 | Najlepszym miejscem rejestracji fabryki jest plik ``conftest.py``. 812 | 813 | .. code-block:: python 814 | 815 | # tests/factories.py 816 | import factory 817 | 818 | class AuthorFactory(factory.Factory): 819 | 820 | class Meta: 821 | model = Author 822 | 823 | class GroupForSuperUserFactory(factory.Factory): 824 | 825 | class Meta: 826 | model = Group 827 | 828 | 829 | # tests/conftest.py 830 | from pytest_factoryboy import register 831 | from .factories import AuthorFactory, GroupForSuperUserFactory 832 | 833 | register(AuthorFactory) 834 | register(GroupForSuperUserFactory) 835 | 836 | 837 | # tests/test_models.py 838 | def test_factory_fixture(author_factory): 839 | author = author_factory(name="Charles Dickens") 840 | assert author.name == "Charles Dickens" 841 | 842 | def test_factory_fixture(group_for_super_user_factory): 843 | author = group_for_super_user_factory(name="Super Group") 844 | assert author.name == "Super Group" 845 | 846 | 847 | Istnieje również możliwość rejestracji modelu pod określoną nazwą wraz z ustawionymi parametrami. 848 | 849 | 850 | .. code-block:: python 851 | 852 | register(BookFactory) # book 853 | register(BookFactory, "second_book") # second_book 854 | 855 | register(AuthorFactory) # author 856 | register(AuthorFactory, "second_author") # second_author 857 | 858 | register(AuthorFactory, "male_author", gender="M", name="John Doe") 859 | register(AuthorFactory, "female_author", gender="F") 860 | 861 | register(BookFactory, "other_book") # other_book, book of another author 862 | 863 | @pytest.fixture 864 | def other_book__author(second_author): 865 | """ 866 | Make the relation of the second_book to another (second) author. 867 | """ 868 | return second_author 869 | 870 | @pytest.fixture 871 | def female_author__name(): 872 | """Override female author name as a separate fixture.""" 873 | return "Jane Doe" 874 | 875 | 876 | Fabryki w testach 877 | ----------------- 878 | 879 | Wykorzystująć fabryki w testach mamy możliwość w dwojaki sposób wykorzystania 880 | zarejestrowanego fixture. Pierwszy do podanie pełnej nazwy klasy w konwencji małe litery 881 | oraz podkreśleniem np. mając fabrykę ``GroupForSuperUserFactory`` należy utworzyć fixture 882 | ``group_for_super_user_factory``. W teście będzie to obiekt fabryki, który należy najpierw 883 | wywołać aby utworzyć obiekt z właściwymi wartościami. 884 | 885 | .. code-block:: python 886 | 887 | def test_factory_fixture(group_for_super_user_factory): 888 | assert isinstance(group_for_super_user, GroupForSuperUserFactory) 889 | author = group_for_super_user_factory(name="Super Group") 890 | assert author.name == "Super Group" 891 | 892 | Istnieje również druga możliwość, która pozwala na bezpośrednie utworzenie modelu w teście 893 | bez tworzenia fabryki. Posiłkując się powyższym przykładem, aby utworzyć model dla fabryki 894 | ``GroupForSuperUserFactory`` tworzymy fixture, jednak bez nazwy `factory`, czyli ``group_for_super_user``. 895 | 896 | .. code-block:: python 897 | 898 | def test_factory_fixture(group_for_super_user): 899 | assert isinstance(group_for_super_user, Group) 900 | 901 | .. code-block:: python 902 | 903 | from app.models import Book 904 | from factories import BookFactory 905 | 906 | def test_book_factory(book_factory): 907 | """Factories become fixtures automatically.""" 908 | assert isinstance(book_factory, BookFactory) 909 | 910 | def test_book(book): 911 | """Instances become fixtures automatically.""" 912 | assert isinstance(book, Book) 913 | 914 | @pytest.mark.parametrize("book__title", ["PyTest for Dummies"]) 915 | @pytest.mark.parametrize("author__name", ["Bill Gates"]) 916 | def test_parametrized(book): 917 | """You can set any factory attribute as a fixture using naming convention.""" 918 | assert book.name == "PyTest for Dummies" 919 | assert book.author.name == "Bill Gates" 920 | 921 | 922 | Atrybuty w fixture 923 | ^^^^^^^^^^^^^^^^^^ 924 | 925 | Tworząc testy możemy parametryzować utworzone fabryki poprzez wykorzystanie markera ``parametrize``. 926 | Aby uaktualnić konkretną wartość musimy wykorzystać podwójne podkreślenie wraz z nazwą pola. 927 | 928 | .. code-block:: python 929 | 930 | @pytest.mark.parametrize("author__name", ["Bill Gates"]) 931 | def test_model_fixture(author): 932 | assert author.name == "Bill Gates" 933 | 934 | Czasami konieczne jest przekazanie instancji innego fixture jako wartości atrybutu do fabryki. 935 | Możliwe jest przesłonięcie wygenerowanego urządzenia atrybutów, gdzie żądane wartości 936 | mogą być wymagane jako zależność fixture. Istnieje również leniwy wrapper dla fixture, 937 | które może być użyte w parametryzacji bez definiowania fixture w module. 938 | 939 | .. code-block:: python 940 | 941 | import pytest 942 | from pytest_factoryboy import register, LazyFixture 943 | 944 | @pytest.mark.parametrize("book__author", [LazyFixture("another_author")]) 945 | def test_lazy_fixture_name(book, another_author): 946 | """Test that book author is replaced with another author by fixture name.""" 947 | assert book.author == another_author 948 | 949 | 950 | @pytest.mark.parametrize("book__author", [LazyFixture(lambda another_author: another_author)]) 951 | def test_lazy_fixture_callable(book, another_author): 952 | """Test that book author is replaced with another author by callable.""" 953 | assert book.author == another_author 954 | 955 | 956 | # Can also be used in the partial specialization during the registration. 957 | register(BookFactory, "another_book", author=LazyFixture("another_author")) 958 | 959 | 960 | Przykłady 961 | --------- 962 | 963 | Poniżej przykład w jaki sposób utworzyć pole własnego typu, pozwalający fabryce na generyczne 964 | tworzenie wartości dla wskazanego pola. 965 | 966 | .. code-block:: python 967 | 968 | # fuzzy_geo.py 969 | from factory.fuzzy import BaseFuzzyAttribute 970 | 971 | class FuzzyPoint(BaseFuzzyAttribute): 972 | 973 | def fuzz(self): 974 | return Point(random.uniform(-180.0, 180.0), random.uniform(-90.0, 90.0)) 975 | 976 | 977 | # factories.py 978 | from .fuzzy_geo import FuzzyPoint 979 | 980 | 981 | class UserFactory(factory.django.DjangoModelFactory): 982 | ... 983 | last_location = FuzzyPoint() 984 | 985 | 986 | Poniżej bardziej skomplikowany przykład pokazujący w jaki sposób możemy utworzyć fabrykę 987 | dla użytkownika aplikacji. 988 | 989 | .. code-block:: python 990 | 991 | import random 992 | import datetime 993 | import factory 994 | 995 | from faker import Faker 996 | from django.utils.text import slugify 997 | from ..models import User 998 | 999 | 1000 | fake = Faker('pl_PL') 1001 | 1002 | 1003 | class UserFactory(factory.django.DjangoModelFactory): 1004 | first_name = factory.Faker('first_name') 1005 | last_name = factory.Faker('last_name') 1006 | username = factory.LazyAttribute( 1007 | lambda o: slugify(o.first_name + '.' + o.last_name)) 1008 | email = factory.LazyAttribute( 1009 | lambda o: o.username + "@" + fake.free_email_domain()) 1010 | password = factory.Faker('password', length=10) 1011 | birthday = factory.Faker('date_between_dates', 1012 | date_start=datetime.date(1960, 1, 1), 1013 | date_end=datetime.date(1998, 1, 1)) 1014 | gender = factory.LazyAttribute( 1015 | lambda o: random.choice([User.FEMALE, User.MALE])) 1016 | 1017 | notifications_enabled = True 1018 | region = factory.Faker('region') 1019 | city = factory.Faker('city') 1020 | description = factory.Faker('sentences') 1021 | level = 1 1022 | registration_status = 2 1023 | score = 0 1024 | 1025 | # brands = factory.LazyAttribute(lambda o: random.choice([])) 1026 | profile_photo = 0 1027 | instagram_url = factory.Faker('uri') 1028 | 1029 | class Meta: 1030 | model = 'users.User' 1031 | django_get_or_create = ('username',) 1032 | 1033 | @factory.lazy_attribute 1034 | def date_joined(self): 1035 | return datetime.datetime.now() - datetime.timedelta( 1036 | days=random.randint(5, 50)) 1037 | 1038 | last_login = factory.LazyAttribute( 1039 | lambda o: o.date_joined + datetime.timedelta(days=4)) 1040 | 1041 | is_staff = False 1042 | is_active = True 1043 | is_superuser = False 1044 | 1045 | 1046 | .. _`Factory Boy`: https://factoryboy.readthedocs.io/en/latest/ 1047 | .. _`pytest-factoryboy`: http://pytest-factoryboy.readthedocs.io/en/latest/ 1048 | --------------------------------------------------------------------------------