abelcastro.dev

Test inheritance with python (and Django)

2021-03-26

TestingPytestDjango

My first post

2021-03-25


12
...
78
9

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

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.

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:

  • Django + Postgres + Nginx
  • Markdown support powered by Martor
  • Dockerized development and production setups
  • HTTPS for production

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.