Blog // Exirel.me

Retrouvez tous les articles liés au tag Postgres via le flux rss dédié à ce tag.

Tester une IntegrityError pour un modèle Django

Par Florian Strzelecki - 11:05 - 10.09.2014

Tags : Django, SQL, Unit Testing, Postgres, IntegrityError

Quoi de plus agaçant qu'avoir du code fonctionnel "en vrai", mais dont les tests ne passent pas ? C'est ce qui m'est arrivé hier, alors que je voulais optimiser l'usage de requêtes SQL avec Postgres (solution finalement trouvée avec l'aide de @twidi, un monsieur qui fait des trucs très cool).

Le problème est assez simple : il s'agit de vérifier que votre code lève une IntegrityError lorsqu'une opération ne respecte pas une contrainte d'intégrité, comme une contrainte de clé étrangère invalide sur un item.save(). Prenons les modèles suivant très simple :

from django.db import models

class Category(models.Model):
    title = models.CharField(max_length=118)

class Item(models.Model):
    title = models.CharField(max_length=118)
    category = models.ForeignKey(Category)

La première chose à faire est d'utiliser transaction.atomic autour de l'appel à la méthode save, dans votre code de l'application (et non pas dans le code de vos tests) :

def some_function(invalid_category_pk):
    """The PK is invalid here. No. Matter. What."""
    item = Item(category_id=invalid_category_pk)

    with transaction.atomic():
        item.save()

Si vous utilisez la commande shell, vous verrez l'exception IntegrityError, et passerez le cœur léger à l'écriture des tests unitaires.

Oui, mais.

Si comme moi vous cherchez une classe de base fournie par Django, vous aurez probablement envie d'utiliser TestCase. Cela tombe bien, c'est une sous-classe de TransactionTestCase. Cela devrait fonctionner. N'est-ce pas ? Hein ?

Non. Oh que non.

Aussi étrange que cela m'est apparu au premier abord, si vous utilisez TestCase, cela ne fonctionnera pas : cette classe étend TransactionTestCase spécifiquement pour retirer la gestion des transactions. Oui, personnellement, je trouve cela perturbant et trompeur, et je laisse les mauvaises langues dire que mon avis est biaisé par le temps perdu à découvrir et comprendre cela.

Donc, pour tester cette fonction, il suffit de faire ceci :

from django.db import IntegrityError
from django.test.testcases import TransactionTestCase

class TestFunctions(TransactionTestCase):

    def test_some_function_raise(self):
        """Assert some_function raise when category does not exist"""
        with self.assertRaises(IntegrityError):
            some_function(7814567)

Et voilà ! C'était facile. J'ai perdu des heures pour découvrir ça. Fuck my life.

Au passage, voici la docstring de TestCase :

Does basically the same as TransactionTestCase, but surrounds every test
with a transaction, monkey-patches the real transaction management routines
to do nothing, and rollsback the test transaction at the end of the test.
You have to use TransactionTestCase, if you need transaction management
inside a test.