Setting up Pytest for a Django project
Tags: django, python, pytest
Published: 2019-12-07 21:40:04

Hi, my name is Asdrubal Santander, I'm a Backend developer at Mo Tecnologías, and this is a short guide to how we implement testing with Pytest-Django.

### Why Pytest?

[Pytest](https://docs.pytest.org/en/latest/index.html) it is an alternative framework to write tests in Python besides [Unittest](https://docs.python.org/3/library/unittest.html), between its best features it has:

- Easy to write `assert` statements just like any other conditional `assert 1 == 1`. No more assertNotNull, assertDictEquals...
- `Fixtures` to run code before and after each test, set up state of a test, load fixtures and more!
- Better integration with plugins and tools (like pdb, and coverage).
- Improved results, when a test fail it tells you exactly where it fails and with colors!
- Can run tests written with the syntax of Unittest. So it makes easy to change.

## Our Project structure:

```
project_name
├── project
│   ├── apps
│   │   ├── integration_apps
│   │   |   ├── app_name
|   |   |   |   └── tests
|   |   |   |       ├── component_to_be_tested
|   |   |   |       |   ├── test_function_name.py
|   |   |   |       |
|   |   |   |       ├── conftest.py
|   |   |   |       └── ...
|   |   |   └── ...
|   |   └── ...
│   ├── project_settings
│   |   └── settings
|   |       ├── development_settings
|   |       ├── production_settings
|   |       └── test_settings
|   └── manage.py
├── pytest.ini
└── ...
```

So as you can see, our Django project as an uncommon structure, I will try to explain in a general way:

- `project_name`: config files, requirements folder, README.MD ...
- `project`: `apps`, fixtures folder, `project_settings` and `manage.py`.
- `project_settings`: `settings`, project urls.py, wsgi.py.
- `settings`: settings for our project in this case we are going to be focus on `test_settings`.
- `apps`: contains our business logic in separated apps.
- `test`: inside this folder we have separated `conftest.py` to an specific app, and is where we are going to put all our tests, separated by folders such like "views", "services", "selectors"...


#### Installation:

```
pip install pytest-django
pip install pytest-cov
pip install pytest-pythonpath
```

- `pytest-django`: A plugin to easily integrate Pytest with Django.
- `pytest-cov`: Plugin that helps to create the coverage reports inside Pytest.
- `pytest-pythonpath`: Plugin that modifies the env var PYTHONPATH. This is useful when pytest can't find your manage.py

#### pytest.ini:

```
[pytest]
addopts = -v -p no:warnings --nomigrations --cov=. --no-cov-on-fail
DJANGO_SETTINGS_MODULE = project.project_settings.settings.test_settings
python_paths = .
```

With the file `pytest.ini` we can set arguments and other configurations, so we don't have to specify each time we run Pytest.

- `addopts`: Special variable to set command line options
  - `-v`: To tell pytest to be a little bit more verbose
  - `-p no:warnings`: Disable warnings.
  - `--nomigrations`: Disable the migrations when run pytest this could make the setup for testing faster.
  - `--cov=.`: When pytest runs it makes a coverage report.
  - `--no-cov-on-fail`: Don't show the coverage report if a test fails.

- `DJANGO_SETTINGS_MODULE`: This is where we tell pytest to run with a specific settings.

- `python_paths`: The dot meaning that the current directory will be added to the PYTHONPATH env var.

#### Running Pytest:

```
$ pytest 

======================================== test session starts ========================================
platform darwin -- Python 3.6.0, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- 
...
cachedir: .pytest_cache
Django settings: project.project_settings.settings.test_settings (from ini file)
rootdir: .../mysite, inifile: pytest.ini
plugins: django-3.7.0, pythonpath-0.7.3, cov-2.8.1
collected 0 items



---------- coverage: platform darwin, python 3.6.0-final-0 -----------
Name                                                                Stmts   Miss  Cover
---------------------------------------------------------------------------------------
...
mo_services/apps/importantapp/test/services/test_function_name.py       0      0   100%
...
---------------------------------------------------------------------------------------
TOTAL                                                                   X     X    XXX%


======================================= no tests ran in 0.09s =======================================
```

Let's write a example test:

Inside of `app_name` we create a python package named `tests`, this package will have all the tests for this application divided in sub packages with the name of the component:

```python
# .../apps/integration_apps/app_name/tests/component_to_be_tested/test_function_name.py

from app_name.component_name import some_function


class TestFunctionName:
  
    def test_example():
        result = some_function()
        
        expected_result = 'some_result'
        
        assert result == expected_result

```

Running pytest again:

```
$ pytest 
======================================== test session starts ========================================
platform darwin -- Python 3.6.0, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- 
...
cachedir: .pytest_cache
Django settings: project.project_settings.settings.test_settings (from ini file)
rootdir: .../mysite, inifile: pytest.ini
plugins: django-3.7.0, pythonpath-0.7.3, cov-2.8.1
collected 1 item

apps/integration_apps/app_name/tests/component_to_be_tested/test_function_name.py::TestFunctionName::test_example PASSED [100%]

---------- coverage: platform darwin, python 3.6.0-final-0 -----------
Name                                                                Stmts   Miss  Cover
---------------------------------------------------------------------------------------
...
mo_services/apps/importantapp/test/services/test_function_name.py       1      0   100%
...
---------------------------------------------------------------------------------------
TOTAL                                                                   X     X    XXX%

========================================= 1 passed in 0.11s =========================================
```

So now you have successfully run Pytest in Django!

#### Some final cool command line options:

- `-x`: This option tells pytest to stop after the first test fail, is great when we are having problems with several tests so in this way we can solve this problems one by one.
- `--nf`: Tells pytest to run the "new tests" first.
- `--ff`: Tells pytest to run the "fail tests" first.
- `--lf`: Tells pytest to run the only the "last fail".


#### Where to find more:

- [pytest: helps you write better programs — pytest documentation](https://docs.pytest.org/en/latest/index.html)
- [Python Testing - Python Software Development and Software Testing (book, posts and podcast)](https://pythontesting.net/)
- [The Django Test Driven Development Cookbook - Singapore Djangonauts](https://youtu.be/41ek3VNx_6Q?list=PLPwQzG2-AU0VL688UCj1oQeCRPqtZB2ji)