abelcastro.dev

Using variables in the Dockerfile FROM statement

2021-06-01

Docker

Create and use "dummy" Models in a Test Case in Django

2021-04-17

TestingDjango

Test inheritance with python (and Django)

2021-03-26

TestingPytestDjango
12
...
8
9
10

Abel Castro 2026 - checkout the source code of this page on GitHub - Privacy Policy

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

  • pass a build_image arg with a default value
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

  • use the passed build_image arg in the FROM statement
# 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

  • Pass the desired value in .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:

  • api/athletes/basketball-player/[slug]
  • api/athletes/soccer-player/[slug]

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.