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!
pip install factory_boy
pip install pytest
pip install pytest-django
pip install pytest-cov
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:
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.
unit_test_settings.py
alongside our projects settings.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.
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.
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!
.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:
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:
.coveragerc
file:[run] omit = *tests* *migrations* */unit-test-settings.py
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.
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!
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 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.
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)
factories.py
):class YearFactory(factory.django.DjangoModelFactory): start_year = 2018 end_year = 2019 label = 18-19 active = True
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!
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.
class ThingFactory(factory.django.DjangoModelFactory): class Meta: model = other_app.Thing attribute = True ...more class attributes setting up default data...
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 duringdata_setup
or you may run into issues.
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.
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.
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)
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!
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.
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!
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_boy
pip install pytest
pip install pytest-django
pip install pytest-cov
pip 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
:
data_setup.py
instantiate any classes needed to generate groups of objects where/when/if multiple objects are requiredTest 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