This demonstrates how to use a variable as image for the Dockerfile FROM statement.
Given this docker-compose.yml / Dockerfile setup:
docker-compose.yml
version: '3'
services:
postgres:
build:
context: .
dockerfile: ./compose/postgres/Dockerfile
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
volumes:
postgres_data:
Dockerfile
FROM postgres:12.3
The Postgres service will always use the same FROM image defined in the Dockerfile. If we want instead to set the FROM image using a variable, we can do the following:
Docker-compose.yml
version: '3'
services:
postgres:
build:
context: .
dockerfile: ./compose/postgres/Dockerfile
args:
build_image: "${BUILD_IMAGE:-postgres:12.3}"
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
volumes:
postgres_data:
Dockerfile
# this will be the default image
ARG build_image="postgres:12.3"
# The default image will be overriden if other build image is passed as ARG
ARG build_image=$build_image
FROM $build_image
.env
BUILD_IMAGE=postgis:9.6
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.