Django is fantastic at a lot of things - it’s secure and has pretty much all the bells and whistles you can imagine. It’s got a great community - but sometimes there are things that are a little more niche and hard to find information on.
Testing in Django (especially with unmanaged models) is something I’ve seen show up on StackOverflow enough to recognize I’m not alone. I’ll guide you on how I prefer to setup testing in Django in a way that makes life a little easier for those of us with more niche setups.
Testing can be tricky in these niche situations - but this guide should get an application to the point of testability without too many hoops to jump through (and only a few libraries to add). This document tries to be exhaustive in explanation of what’s going on - but for those more interested in getting to it I’ve included a TLDR Summary.
I have had to deal with a lot of unmanaged tables and views - luckily there are
libraries like factory_boy and pytest (with the pytest-django plugin)
that make this much easier to manage. So let’s get to it!
Required libraries
Factory_boy
pip install factory_boy
Pytest
pip install pytest
Pytest-django
pip install pytest-django
Additional (optional) libraries
Pytest-cov
pip install pytest-cov
Pytest-html
pip install pytest-html
Additional Note: You can also setup your tests to run (without migrations) with the default Django Test Suite! I’ve outlined the steps a little to help - although this whole document is a great primer and example of testing our projects.
We will need to perform a little setup ahead of time prior to running our tests:
- Setup our database to run a local sqlite version for testing only
- Setup Pytest
- Allow django to manage unmanaged models (but only when testing!)
- Setup our factories appropriately
- Setup our data for testing
Database Setup
Know your database setup!
Some places have really locked-down databases and keep things locked down to a
point where Django cannot create the test databases it normally would (which are
just empty copies of the database(s) specified in settings.py) This is true
especially when dealing with MSSQL (as it would require giving django rights to
master tables which isn’t an option in certain organizations).
We can bypass a lot of this drama by setting up the database to use SQLite - although there are times or apps which require things that might be database specific. In those cases, working with your DB admin to allow django appropriate access is more important. I have found for most basic testing though, SQLite fits the bill for testing. It is also nice, because it works quickly and only uses local data so we’re never touching anything on the outside of our machine. This also allows Django to manage the data and create/teardown the databases on an as-needed basis.
Luckily for us - pytest and pytest-django make this process pretty simple!
In order to use SQLite databases, we will tell pytest to use a settings file we
will create, which will explicitly tell django to use SQLite.
- Create a file named
unit_test_settings.pyalongside our projectssettings.py
Note: You can use this Github Gist as the guideline and replace ‘your_project’ where necessary
Multiple Databases: If your project uses multiple databases then you can
add those in the unit_test_settings.py file. If your project uses models
which require other databases - you’ll need to make sure you are creating the
appropriate sqlite database(s) named accordingly.
Pytest Setup
Create a file named pytest.ini in the root of our project (alongside manage.py).
This file will tell pytest which settings/options we want to use when testing.
This will allow you to setup commands in the .ini file to run when you run pytest
instead of having to add all the flags individually in the command line.
Example Command/Useage
pytest - this will run tests for your entire project and display results in
terminal.
pytest --cov=. - this will run tests against the project, display results in
terminal and create a report in the directory htmlcov documenting coverage
relating to the entire project.
pytest --cov=appname - this will run tests against the app, display results
in terminal and create a report in the directory htmlcov documenting coverage
relating to that app.
In some cases we would end up with a lot of additional command line arguments -
but we can simplify it by using that .ini file!
Example .ini file
[pytest]
addopts =
--nomigrations
--cov-config=.coveragerc
--cov=your_project
--cov=your_app
--cov=another_app_in_the_project
--junitxml=./test-results/junit.xml
--cov-report html:./test-results/htmlcov
--html=./test-results/test_results.html
--self-contained-html
DJANGO_SETTINGS_MODULE = your_project.unit_test_settings
python_files = tests.py test_*.py *_tests.py
That will:
- Set it up so the migrations aren’t ran (so our unmanaged models don’t cause errors - if they don’t have migrations)
- Run coverage for the specified project/apps
- Give us a junit.xml file which will be used by various CI/CD
- Give us an HTML page that shows passing/failing/errored tests
Nice!
Note: We specify a .coveragerc file in this .ini file - you may have
to create this and set it up how you need. I generally just use it to omit
certain files from being tested (like migrations, or the unit-test-settings.py
file). There is good information out there on setting this up which is beyond
the scope of this article, but in order to be a little more thorough here is an
example:
Example .coveragerc file
[run]
omit =
*tests*
*migrations*
*/unit-test-settings.py
Allow Django To Manage Unmanaged Models
Note: With the unit-test-settings.py being specified, this may not be a
requirement - testing is required to confirm this! Doing the following will
ensure unmanaged models will be tested, but may not be necessary.
We can look at our models - as we want Django handling them for testing purposes even if they’re unmanaged.
If a model is unmanaged but used in our application we need to adjust the
Meta subclass of the unmanaged model.
Only adjust the models your application needs! For example, if your application uses classes from another project, only adjust the models of that project that you’re using in your current application.
Unmanaged Model Example
class UnmanagedModel(Model):
id = models.IntegerField(db_column='ID', primary_key=True)
year = models.IntegerField(db_column='year')
grade = models.CharField(db_column='grade', max_length=2)
things = models.IntegerField(db_column='things')
class Meta(object):
managed = False
db_table = '[database].[databaseTable]'
We need to change the managed attribute if we are under test! We can do that
by changing it to this:
managed = getattr(settings, 'UNDER_TEST', False)
This will set managed to False, unless we’re UNDER_TEST, in which case it
will return True and Django / Pytest can manage the model as it sees fit
(so tables for this model will be created in the local SQLite, which we can
then fill with data).
Note: If your model pulls data from a pre-populated table, or a view - this step is the same!
Factory Setup
This is where we make creating dummy data easy! We create factory versions of
our classes, which allow us to create testable data that we are in total
control of. Absolutely no randomness (unless we specifically desire that).
Factory_Boy is the library that will help us achieve this.
There is some great documentation with factory_boy:
In a nutshell? Any model that you create should have a corresponding Factory
class.
Even if your model is unmanaged - create the Factory for it. It gives you a
local version of that model in your test environment with the criteria that
you’ve setup - making it controlled and testable.
Setup for factory_boy is pretty simple:
pip install factory_boy
Create a factories.py file in the directory of your app (not project level)
Begin building your factory models inside of there (remember to import your application classes from any models you wish to test against - your_other_app, another_app, etc.)
Managed Models
Managed models are super simple to create a factory for. They look identical - except in the factory you’re filling in information you want your dummy object to have.
Standard Django Model(models.py)
class Year(models.Model):
start_year = models.IntegerField()
end_year = models.IntegerField(primary_key=True)
label = models.CharField(max_length=6)
active = models.BooleanField(default=False)
Factory_boy Model (factories.py)
class YearFactory(factory.django.DjangoModelFactory):
start_year = 2018
end_year = 2019
label = 18-19
active = True
Example Test
class YearTest(unittest.TestCase):
def test_active_year_exists(self):
# Create a new Year model by calling the Factory
year = YearFactory()
# Our Factory sets active to True - we can test that it is!
self.assertTrue(year.active == True)
This test would pass!
Unmanaged Models
If a model is unmanaged? No worries, we can still get it under test! Since the
managed flag in the Meta subclass will be set to ‘True’ in testing - pytest
will automatically handle the table creation when we run our tests (as long as
we’re using the --nomigrations flag which is setup in the pytest.ini so it
does it automatically).
Data From A Table:
Data coming from a table or view is easy to create factories for. You can
simply inherit from factory.django.DjangoModelFactory for the factories
relating to those models.
Factory Example
class ThingFactory(factory.django.DjangoModelFactory):
class Meta:
model = other_app.Thing
attribute = True
...more class attributes setting up default data...
Notes Regarding Factories
Classes that inherit from django.DjangoModelFactory are created and saved
upon instantiation. If you call MyClassFactory() in the test? You now have an
instance of MyClass as well as instances of any other classes that MyClass
is tied to via foreign key.
Classes that inherit from factory.Factory must be created and then saved by
using .save() - it’s an extra step but is required for data that is read from
a MSSQL view. Since our test models are inheriting from DjangoModelFactory
this shouldn’t ever be an issue but could be a ‘gotcha’ and is worth note.
Data provided by any MSSQL view (or table that is read from) that we want to have
dummy data for will most likely have to be built ahead of time / populated in
advance during data_setup or you may run into issues.
Foreign Keys
A SubFactory allows you to make Foreign Keys for objects easily. Using the
ThingFactory example above (from Unmanaged Models) you
could make a foreign key to Year like this:
year = factory.SubFactory(YearFactory)
Now if you create a Thing model (ThingFactory()), factory_boy will
automatically create a Year model as well, each filled with the dummy data
you setup in the factory.
Data Setup
Some models are tied to databases that are pre-populated with data. How do we
get around that and create similar data in a testable way in our test
environment? Create a data_setup.py file where can create functions to set up
chunks of required data!
Create a data_setup.py file in the tests directory of your application (
if you don’t have a tests directory - create one!)
Instantiate all of your required classes to setup all the dummy data in a
function that we can call from our tests setUp or setUpTestData methods.
Example Data_Setup.py:
from your_app import models as app_models
import your_app.factories as f
import factory
def basic_setup(self):
# setup your required pre-populated data here
f.ThingFactory()
There may be cases where you need more data than just one instance of a
particular model. Maybe (for example) you needed multiple Year objects
created. Just create an additional function in data_setup.py and call it in
your basic_setup function.
Example build_years Function
def build_years(self):
# Create 4 years back from current year
active_year = pcm.Year.get_active_end_year()
for i in range(0, 4):
end_year = active_year
end_year = end_year - i
start_year = end_year - 1
f.YearFactory(
start_year=start_year,
end_year=end_year,
label=str(start_year)[-2:]+'-'+str(end_year)[-2:],
active=False
)
data_setup Using build_years
def basic_setup(self):
self.user = f.UserFactory()
self.year = f.YearFactory()
# Create the rest of our years with build_years
build_years(self)
Using Data_Setup
In your test file - import your test factories from your application
import app_name.factories as f
In your tests setUp method - call the basic_setup function and your dummy
data is loaded and ready for testing!
class EmployeeStaffingGroupTest(TestCase):
def setUp(self):
data_setup.basic_setup(self)
def test_employee_staffing_group(self):
employee_staffing_group = f.StaffingGroupFactory()
expected_employee_staffing_group = 'Staffmember, Test'
self.assertEquals(
expected_employee_staffing_group, str(employee_staffing_group))
That test would pass!
You can use that setUp method on any TestCase where you need dummy data
populated for testing purposes.
NOTE: Make sure that your tests start with test_ or it will not run!
Running Our Tests
Since we’re using pytest, running our tests is pretty simple.
From the project directory (same directory where you run any manage.py
commands) just run the following command:
pytest
It will run the commands from the pytest.ini file that we setup earlier. If
you wish to run pytest against a particular app? Just run:
pytest yourappname --cov=appname
You will see results with how many tests have passed, failed, or errored out. You’ll also be shown a number of warnings if you’re using deprecated verbiage or other such items.
Note: If you have pytest installed in your root Python install (and also in your virtualenv) it will try to use your root install by default! This will cause errors.
Standard Django Testing (without Migrations)
Our unit-test-settings.py would look like the Github Gist
Then you can run the tests in standard Django fashion:
python manage.py test --settings=”your_project.unit-test-settings”
The tests should all run and pass the way you would expect. It is the same as with pytest at this point - although pytest we see the generated coverage!
TL;DR : Summary
This summary is meant to be short and sweet. If you need clarification on any of these steps - please see the appropriately linked sections.
Install factory_boy, pytest, pytest-django, pytest-cov and pytest-html:
pip install factory_boypip install pytestpip install pytest-djangopip install pytest-covpip install pytest-html
Create and setup unit-test-settings.py and pytest.ini
Set databases to use local SQLite databases for testing
Change unmanaged models to be managed when UNDER_TEST variable is True:
managed = getattr(settings, 'UNDER_TEST', False)
Create factories.py in the directory of your app (not project level)
Build your Model Factories (inheriting from factory.Factory or
factory.django.DjangoModelFactory where appropriate - depending if the data
is coming from a table or a view)
Create a data_setup.py file in the tests directory of your application if
needed
If using data_setup.py:
- In
data_setup.pyinstantiate any classes needed to generate groups of objects where/when/if multiple objects are required
Test your application!
Note: You should be able to test it with either/both of the following commands:
python manage.py test --settings=”your_project.unit-test-settings”
or
pytest
Comments
{0xc0004ec000 0xc00011a728}