Django tests - patch object in all tests

22,095

Solution 1

According to the mock documentation:

Patch can be used as a TestCase class decorator. It works by decorating each test method in the class. This reduces the boilerplate code when your test methods share a common patchings set.

This basically means that you can create a base test class with @patch decorator applied on it that would mock your external calls while every test method inside would be executed.

Also, you can use start() and stop() patcher's methods in setUp() and tearDown() methods respectively:

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

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

Solution 2

Just to add to alecxe's answer, if you are using teardown() then according to the docs

you must ensure that the patching is “undone” by calling stop. This can be fiddlier than you might think, because if an exception is raised in the setUp then tearDown is not called.

If an exception is raised in your tests, your patching won't be undone. A better way would be to call addCleanup() inside your setUp(). Then you can omit the tearDown() method altogether.

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

Solution 3

I ended up creating a test runner to serve my purpose. I needed to mock the file storage so that images do not actually write to the file system while testing. The images object is being called in many tests thus patching each class would not be DRY. Also, I noticed that mocking the file itself would leave it on the system in case the test failed. But this method didn't.

I created a file runner.py in the project root

# 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)

Then I would run my tests using python manage.py tests --testrunner=runner.UnitTestRunner


Just for clarity the ImageFactory.get_image method is a custom method

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}')
Share:
22,095
tunarob
Author by

tunarob

Updated on January 24, 2021

Comments

  • tunarob
    tunarob over 3 years

    I need to create some kind of MockMixin for my tests. It should include mocks for everything that calls external sources. For example, each time I save model in admin panel I call some remote URLs. It would be good, to have that mocked and use like that:

    class ExampleTestCase(MockedTestCase):
        # tests
    

    So each time I save model in admin, for example in functional tests, this mock is applied instead of calling remote URLs.

    Is that actually possible? I'm able to do that for 1 particular test, that is not a problem. But it'd be more useful to have some global mock because I use it a lot.

  • tunarob
    tunarob over 9 years
    And that also means I have to decorate each of my test cases, and not the Mixin test class. It's also quite inconvenient that I have to put additional parameter for each test method. But it's better than nothing.
  • alecxe
    alecxe over 9 years
    @galozek please see the relevant topics: here and here.
  • tunarob
    tunarob over 9 years
    the 2nd solution worked. I tried that approach but was missing start()/stop() part. Thanks.
  • Dan
    Dan almost 8 years
    tearDown will always be called, even if your tests raise an exception.
  • Meistro
    Meistro almost 8 years
    From the docs: "If setUp() fails, meaning that tearDown() is not called, then any cleanup functions added will still be called." So it's still better to call addCleanup than hope that no exceptions are raised in setUp.
  • Dustin Wyatt
    Dustin Wyatt about 6 years
    Note that you might be better off using self.addCleanup(self.patcher.stop) instead of doing it in tearDown since cleanup gets run whether or not there is an exception. See @Meistro's answer.
  • Stevoisiak
    Stevoisiak almost 6 years
    Don't forget to include imports: from unittest import TestCase, from import unittest.mock import patch
  • Stevoisiak
    Stevoisiak almost 6 years
    Don't forget to include imports: from unittest import TestCase, from import unittest.mock import patch
  • Clint Eastwood
    Clint Eastwood over 3 years
    wrt what @Meistro mentioned above... make sure that the cleanup hook is added ASAP within the setUp method, otherwise, if it fails before the hook is added, it'll be useless