Django TDD setup

Unit Tests

Setup

1. Create virtual env. pipenv shell
2. Install django pipenv install django
3. Create django project. django-admin startproject tested
4. Create .gitignore file touch .gitignore

settings

setup a settings file for tests. This will use an in-memory sqlite DB (sqlite3). This is good because we don’t want a PG or MySQL db running i/o operation on the hard-drive. We have 100s of tests and db needs to be setup and teardown for each test. So and in-memory db like sqlite is the best choice for test database. I also add EMAIL_BACKEND in it to set the email backend to local so it accidentally does not start sending emails.

Install pytest and plugins

Install pytest and related plugins

pipenv install pytest pytest-cov pytest-django mock pytest-factoryboy

pytest-cov is for generating a coverage report that is based on how much of your code is covered by the tests.
mock is a third party mocking application that allows one to create an API of payment gateways and other services
pytest-factoryboy generates testing data including populating the database with random data.

pytest.ini

Next step is to put a pytest.ini file. You put it at the root of the project, same level as manage.py

addopts are the options that one may add on the command line interface (CLI). We want no migrations for the database and we want a coverage report in HTML. Other options are available here – http://pytest-cov.readthedocs.io/en/latest/readme.html#reporting

At this point, you can try to see if tests can be launched.

In terminal, where pytest.ini is located, run: pytest

.coveragerc

Since we want 100% coverage, we put a .coveragerc file to indicate what files we don’t intend to be included in coverage.

this file is at same level as pytest.ini file.

Test procedure

The test setup is now complete. pytest will find all files called “test_*.py”. In these files, pytest will execute all functions called “test_*()” on all classes that start with Test

Each django app will have a tests folder. For each code in apps, like forms.py, we will have test_forms.py in the test folder.

Write first app and run first test

python manage.py startapp birdie
Update settings.py file to include 'birdie'

Wiring for pytest-factoryboy

inside birdie, remove tests.py since we don’t want to pust all our tests in one file. We will create a folder tests and make it a python module by making __init__.py inside it.

Next we create a file called factories.py. In this file we setup all the factories for generating test data, like generating model objects for testing using the faker library. Factories look very similar to models.

Next, we create a conftest.py file where we register the factories.
Fixtures defined in conftest.py file will be recognized in any test file. “Registering” the factories lets you inject them into your tests with a lowercase-underscore class name. In this same file, I’m defining fixtures with @pytest.fixture() which I’ll use in my tests, and injecting them with the factories I just registered.

Test models

Now, we will create one test_*.py file for each code file. For example, if we have models.py file, we will create test_models.py file.

Since most apps have a model, lets start with writing a test for model.

Test admin

If we create admin interfaces of our models, we also need to write test for admin interface of models.
One important trivia is that to access admin interface, we need to instantiate AdminSite. Only after we have created this AdminSite() instance, can we create an instance of PostAdmin. This is based on pure observation.

For example:

has the corresponding admin:

Test views

For testing views, we use Django’s RequestFactory. Like its name suggests, its a factory that will generate a Django request object for you. RequestFactory allows us to instantiate our view classes. In other words, the views we instantiate for testing only work only if we pass in a request. We need to create a fake request object that we can pass into our view. This is what a RequestFactory is good for.We can use something like Selenium but that makes testing very slow. The con of using RequestFactory is that it does not test the Django templates but that is something I can live with. I will manually test each page in the browser anyways.

Some tips:

  • we can use self.client.get() to do a get request on any view but this is very slow, almost like using selenium
  • we can instantiate our class based views just like we do it in our urls.py, via ViewName.as_view(). This turns our view class into a functions and then we can use this request object as that function’s arguement.
  • To test our views, we create a Request, pass it into our View, then make assertions on the returned Response.

Example test view:

A small trivia is that in this code RequestFactory().get('/'), we can put anything in the url, here we put ‘/’. The url never gets evaluated. Even if we had kept url part empty like get(”) or garbage like get(‘asdkjfasf’), the test will still pass.

And here is the view:

Note that I don’t even need to have the actual html file as RequestFactory never evaluates the url or templates. The test will still pass even if there is no file called ‘home.html’ in templates/birdie folder.

Testing Authentication

  1. We want to create a view that can be only accessed by superusers
  2. We will use the @method_decorator(login_required) trick to protect our view
  3. that means, that there must be a .user attribute on the Request. By default, RequestFactory does not create a request object with the user attribute. So we will have to manually create a user and attach it to the request object.
  4. Even if we want to test as an anonymous user, in that case Django automatically attached a AnonymousUser instance to Request, so we have to fake this as well

Small trivia: Django actually has a class called AnonymousUser but this class does not have an associated database table.

So, first we need a way to fake authenticated users. Here it is:

Step 1: Create a user factory in factories.py:

Step 2: Create a global fixture in contest.py

Step 3: Write your test for the view, lets call it AdminView

Step 4: Create the corresponding view

Testing Forms

Lets imagine that we want to create a form that creates a Post object. Lets call it PostForm.

We can instantiate a PostForm by PostForm(). Since a form takes some data, we can give the data to the form when instantiating like so: PostForm(data={}). Now, usually, if a form is empty, it should not be valid. So this can be our first test. Then, since we are tweeting messages, and we have a minimum character length requirement of 6 for a valid message, we will write tests for too-short and OK messages.

So, lets do the wiring first.

Step 1: First we write fixtures for generating random test, both short and long. we use FuzzyText from factory.fuzzy to generate random text of specified length.

Step 2: We write the test_forms.py file:

Step 3: We write the forms.py file

Testing Post request

  • We want to create a view that uses the PostForm to update a Post
  • Testing POST requests works in the same way like GET requests.
  • Since UpdateView uses Form, we will use the PostForm created above

So, since we are test a PostUpdateView, as in updating a Post that is already present, lets first create a fixture to create the text for that post update.

Add this new fixture to conftest.py

Now, lets write the test in test_views.py. Since the view is going to be called PostUpdateView, we write the test like so:

trivia: refresh_from_db() is a method on all django query objects that do exactly that .
PostForm already exists. So we finally add the view to views.py:

Testing 404, 500 errors

use self.client.get()

Testing Template tags

Testing Django management commands

Testing with sessions

Testing with Files

Like how do you test the image upload field of a form

https://afroshok.com/articles/test-driven-development-pytest/




https://dev.to/aduranil/writing-tests-with-pytest-and-factory-k7
https://pytest-django.readthedocs.io/en/latest/database.html
https://faker.readthedocs.io/en/master/

Functional Tests

We will be using unittest with Selenium, and Firefox.

Download the geckodriver from https://github.com/mozilla/geckodriver/releases

Extract geckodriver executable. I put it in my project root (lets call it my_project. Inside my_project we have manage.py). You can optionally add it to .gitignore as this will never be deployed to production. I tend to not put it in my /usr/local/bin.

So, geckodriver sits here: my_project/geckodriver

pipenv install selenium

Now, before running functional tests, open the shell where you will be running the functional tests. Then add the path to the geckodriver to you $PATH.

export PATH=$PATH:/home/mykonos/Python/projects/my_project

now create a file called functional_tests.py in the same level as manage.py

then run the test like so:

python -m functional_tests




No Comments


You can leave the first : )



Leave a Reply

Your email address will not be published. Required fields are marked *