Let's image we need a new model only for a test case and we don't really want to register in our project. We can create something similar than this:
example_app.tests.test_app.models.TestModel
from django.db import models
class TestModel(models.Model):
field_a = models.IntegerField()
field_b = models.IntegerField()
class Meta:
app_label = 'test_app'
We could try to use TestModel
and create objects in a test case:
test_models.py
from django.test import TestCase
from example_app.tests.test_app.models import TestModel
class TestOverridingInstalledApps(TestCase):
def setUp(self):
self.test_model = TestModel.objects.create(
field_a=1,
field_b=2,
)
def test_objects(self):
self.assertEqual(TestModel.objects.count(), 1)
But if you run the tests like this, the test will fail and return something similar than that:
./manage.py test
django.db.utils.ProgrammingError: relation "test_app_testmodel" does not exist
LINE 1: INSERT INTO "test_app_testmodel" ("field_a", "field_b") VALU...
It fails because Django needs to have TestModel
registered in INSTALLED_APPS but we don't really want to add our example_app.tests.test_app
to INSTALLED_APPS because we only need it when we run the tests.
The solution is to to add the test_app to the settings with modify_settings and calling migrate.
test_models.py
from django.core.management import call_command
from django.test import TestCase, modify_settings
from example_app.tests.test_app.models import TestModel
@modify_settings(INSTALLED_APPS={
'append': 'example_app.tests.test_app',
})
class TestOverridingInstalledApps(TestCase):
def setUp(self):
call_command('migrate', run_syncdb=True)
self.test_model = TestModel.objects.create(
field_a=1,
field_b=2,
)
def test_objects(self):
self.assertEqual(TestModel.objects.count(), 1)
You can see the complete source code here.
Let's image that we have this models:
models.py
class Athlete(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=300, unique=True)
age = models.PositiveIntegerField()
class Meta:
abstract = True
class BasketballPlayer(Athlete):
points_scored = models.PositiveIntegerField()
assists = models.PositiveIntegerField()
rebounds = models.PositiveIntegerField()
def __str__(self):
return self.name
class SoccerPlayer(Athlete):
goals_scored = models.PositiveIntegerField()
assists = models.PositiveIntegerField()
yellow_cards = models.PositiveIntegerField()
def __str__(self):
return self.name
We also provide these 2 GET endpoints:
And we want to test that every endpoint returns a 200 code and also be sure that serialized data looks like we expect.
tests.py
class TestBasketballPlayerAPI(TestCase):
def setUp(self) -> None:
self.basketball_player = create_test_basketball_player()
def test__response_ok(self):
url = reverse_lazy(
"basketball_player", kwargs={"slug": self.basketball_player.slug}
)
response = self.client.get(url)
assert response.status_code == 200
assert response.data == BasketballPlayerSerializer(self.basketball_player).data
class TestSoccerPlayerAPI(TestCase):
def setUp(self) -> None:
self.soccer_player = create_test_soccer_player()
def test__response_ok(self):
url = reverse_lazy("soccer_player", kwargs={"slug": self.soccer_player.slug})
response = self.client.get(url)
assert response.status_code == 200
assert response.data == SoccerPlayerSerializer(self.soccer_player).data
Both test classes TestBasketballPlayerAPI
and TestSoccerPlayerAPI
are very similar. Both check that a 200 is returned and the serialized data.
Let's try to refactor this. We could make a base class AthleteTest
for the tests and inherit from it.
class AthleteTest(TestCase):
view_name = ""
serializer_class = None
def setUp(self) -> None:
self.test_athlete = None
@mark.django_db
def test__response_ok(self):
url = reverse_lazy(
self.view_name, kwargs={"slug": self.test_athlete.slug}
)
response = self.client.get(url)
assert response.status_code == 200
assert response.data == self.serializer_class(self.test_athlete).data
class TestBasketballPlayerAPI(AthleteTest):
view_name = "basketball_player"
serializer_class = BasketballPlayerSerializer
def setUp(self) -> None:
self.test_athlete = create_test_basketball_player()
class TestSoccerPlayerAPI(AthleteTest):
view_name = "soccer_player"
serializer_class = SoccerPlayerSerializer
def setUp(self) -> None:
self.test_athlete = create_test_soccer_player()
The code is now much shorter and easier to extend with new test cases. But if we try to run pytest it will fail.
Pytest will collect 3 test cases and fail because AthleteTest
is not actually a test case and it is only intended to be inherit from it.
In order to avoid this problem we can use the option __test__
. After adding it the test will look like this:
class AthleteTest(TestCase):
__test__ = False
view_name = ""
serializer_class = None
def setUp(self) -> None:
self.test_athlete = None
@mark.django_db
def test__response_ok(self):
url = reverse_lazy(
self.view_name, kwargs={"slug": self.test_athlete.slug}
)
response = self.client.get(url)
assert response.status_code == 200
assert response.data == self.serializer_class(self.test_athlete).data
class TestBasketballPlayerAPI(AthleteTest):
__test__ = True
view_name = "basketball_player"
serializer_class = BasketballPlayerSerializer
def setUp(self) -> None:
self.test_athlete = create_test_basketball_player()
class TestSoccerPlayerAPI(AthleteTest):
__test__ = True
view_name = "soccer_player"
serializer_class = SoccerPlayerSerializer
def setUp(self) -> None:
self.test_athlete = create_test_soccer_player()
Now pytest will only collect 2 test and pass as expected.
Checkout the complete source code here.
Note: this method only works with pytest and NOT with the Django test runner.
Hi Internet! Welcome to my blog. I will write about topics mostly related to Python and in special to Django.
This blog is also built with Django because I thought that if I'm going to write about Django and Python it makes sense that this blog is made with them.
You can find the source code in my GitHub.
It is a simple Django project with a Markdown editor for writing posts in the Django admin. For my me and this blog is exactly what I need but if you are looking for a CMS you should take a look to Wagtail.
Features of my blog project:
Note that the production environment is going to serve the media and static files with Nginx. I took the decision to have this "all-in-one setup" because I am not expecting to have a lot of traffic here. Please consider to use a cloud storage service if you are expecting more visitors in your website.