web-dev-qa-db-fra.com

Django tests - objet patch dans tous les tests

J'ai besoin de créer une sorte de MockMixin pour mes tests. Il doit inclure des simulations pour tout ce qui appelle des sources externes. Par exemple, chaque fois que j'enregistre le modèle dans le panneau d'administration, j'appelle des URL distantes. Ce serait bien de se moquer de ça et de l'utiliser comme ça:

class ExampleTestCase(MockedTestCase):
    # tests

Ainsi, chaque fois que j'enregistre le modèle dans admin, par exemple dans les tests fonctionnels, cette maquette est appliquée au lieu d'appeler des URL distantes.

Est-ce vraiment possible? Je suis capable de le faire pour 1 test particulier, ce n'est pas un problème. Mais il serait plus utile d'avoir une maquette globale parce que je l'utilise beaucoup.

29
tunarob

Selon la documentation mock :

Le patch peut être utilisé comme décorateur de classe TestCase. Il fonctionne en décorant chaque méthode de test de la classe. Cela réduit le code passe-partout lorsque vos méthodes de test partagent un ensemble de correctifs commun.

Cela signifie essentiellement que vous pouvez créer une classe de test de base avec un décorateur @patch Qui se moquerait de vos appels externes tandis que chaque méthode de test à l'intérieur serait exécutée.

Vous pouvez également utiliser start() et stop() les méthodes de patcher dans les méthodes setUp() et tearDown() respectivement:

class BaseTestCase(TestCase):
    def setUp(self):
        self.patcher = patch('mymodule.foo')
        self.mock_foo = self.patcher.start()

    def tearDown(self):
        self.patcher.stop()
40
alecxe

Juste pour ajouter à la réponse d'alecxe , si vous utilisez teardown() alors selon la documentation

vous devez vous assurer que le correctif est "annulé" en appelant stop. Cela peut être plus compliqué que vous ne le pensez, car si une exception est déclenchée dans setUp, alors tearDown n'est pas appelée.

Si une exception est levée dans vos tests, votre correction ne sera pas annulée. Une meilleure façon serait d'appeler addCleanup() à l'intérieur de votre setUp(). Vous pouvez ensuite omettre complètement la méthode tearDown().

class BaseTestCase(TestCase):
    def setUp(self):
        self.patcher = patch('mymodule.foo')
        self.mock_foo = self.patcher.start()
        self.addCleanup(self.patcher.stop) # add this line
22
Meistro

J'ai fini par créer un testeur pour servir mon objectif. Je devais simuler le stockage de fichiers afin que les images n'écrivent pas réellement dans le système de fichiers pendant le test. L'objet images est appelé dans de nombreux tests, donc patcher chaque classe ne serait pas DRY. De plus, j'ai remarqué que se moquer du fichier lui-même le laisserait sur le système au cas où le test échouerait. Mais cette méthode n'a pas fonctionné.

J'ai créé un fichier runner.py dans la racine du projet

# runner.py
from unittest.mock import patch

from Django.test.runner import DiscoverRunner

from myapp.factories import ImageFactory


class UnitTestRunner(DiscoverRunner):

    @patch('Django.core.files.storage.FileSystemStorage.save')
    def run_tests(self, test_labels, mock_save, extra_tests=None, **kwargs):
        mock_save.return_value = ImageFactory.get_image()
        return super().run_tests(test_labels, extra_tests=None, **kwargs)

Ensuite, j'exécutais mes tests en utilisant python manage.py tests --testrunner=runner.UnitTestRunner


Pour plus de clarté, le ImageFactory.get_image méthode est une méthode personnalisée

from Django.core.files.base import ContentFile
from factory.Django import DjangoModelFactory
from io import BytesIO
from PIL import Image as PilImage
from random import randint

class ImageFactory(DjangoModelFactory):

    @classmethod
    def get_image(cls, name='trial', extension='png', size=None):
        if size is None:
            width = randint(20, 1000)
            height = randint(20, 1000)
            size = (width, height)

        color = (256, 0, 0)

        file_obj = BytesIO()
        image = PilImage.new("RGBA", size=size, color=color)
        image.save(file_obj, extension)
        file_obj.seek(0)
        return ContentFile(file_obj.read(), f'{name}.{extension}')
0
giantas