Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Better way to mock class attribute in python unit test

I have a base class that defines a class attribute and some child classes that depend on it, e.g.

class Base(object):
    assignment = dict(a=1, b=2, c=3)

I want to unittest this class with different assignments, e.g. empty dictionary, single item, etc. This is extremely simplified of course, it's not a matter of refactoring my classes or tests

The (pytest) tests I have come up with, eventually, that work are

from .base import Base

def test_empty(self):
    with mock.patch("base.Base.assignment") as a:
        a.__get__ = mock.Mock(return_value={})
        assert len(Base().assignment.values()) == 0

def test_single(self):
    with mock.patch("base.Base.assignment") as a:
        a.__get__ = mock.Mock(return_value={'a':1})
        assert len(Base().assignment.values()) == 1

This feels rather complicated and hacky - I don't even fully understand why it works (I am familiar with descriptors though). Does mock automagically transform class attributes into descriptors?

A solution that would feel more logical does not work:

def test_single(self):
    with mock.patch("base.Base") as a:
        a.assignment = mock.PropertyMock(return_value={'a':1})
        assert len(Base().assignment.values()) == 1

or just

def test_single(self):
    with mock.patch("base.Base") as a:
        a.assignment = {'a':1}
        assert len(Base().assignment.values()) == 1

Other variants that I've tried don't work either (assignments remains unchanged in the test).

What's the proper way to mock a class attribute? Is there a better / more understandable way than the one above?

like image 784
Ivo van der Wijk Avatar asked Mar 11 '14 11:03

Ivo van der Wijk


People also ask

How do you mock a unit test in Python?

If you want to mock an object for the duration of your entire test function, you can use patch() as a function decorator. These functions are now in their own file, separate from their tests. Next, you'll re-create your tests in a file called tests.py .

Can we mock a class in Python?

Once you patch a class, references to the class are completely replaced by the mock instance. mock. patch is usually used when you are testing something that creates a new instance of a class inside of the test.


4 Answers

base.Base.assignment is simply replaced with a Mock object. You made it a descriptor by adding a __get__ method.

It's a little verbose and a little unnecessary; you could simply set base.Base.assignment directly:

def test_empty(self):
    Base.assignment = {}
    assert len(Base().assignment.values()) == 0

This isn't too safe when using test concurrency, of course.

To use a PropertyMock, I'd use:

with patch('base.Base.assignment', new_callable=PropertyMock) as a:
    a.return_value = {'a': 1}

or even:

with patch('base.Base.assignment', new_callable=PropertyMock, 
           return_value={'a': 1}):
like image 151
Martijn Pieters Avatar answered Oct 23 '22 14:10

Martijn Pieters


Perhaps I'm missing something, but isn't this possible without using PropertyMock?

with mock.patch.object(Base, 'assignment', {'bucket': 'head'}):
   # do stuff
like image 29
igniteflow Avatar answered Oct 23 '22 14:10

igniteflow


To improve readability you can use the @patch decorator:

from mock import patch
from unittest import TestCase

from base import Base

class MyTest(TestCase):
    @patch('base.Base.assignment')
    def test_empty(self, mock_assignment):
        # The `mock_assignment` is a MagicMock instance,
        # you can do whatever you want to it.
        mock_assignment.__get__.return_value = {}

        self.assertEqual(len(Base().assignment.values()), 0)
        # ... and so on

You can find more details at http://www.voidspace.org.uk/python/mock/patch.html#mock.patch.

like image 10
Dan Keder Avatar answered Oct 23 '22 16:10

Dan Keder


If your class (Queue for example) in already imported inside your test - and you want to patch MAX_RETRY attr - you can use @patch.object or simply better @patch.multiple

from mock import patch, PropertyMock, Mock
from somewhere import Queue

@patch.multiple(Queue, MAX_RETRY=1, some_class_method=Mock)
def test_something(self):
    do_something()


@patch.object(Queue, 'MAX_RETRY', return_value=1, new_callable=PropertyMock)
def test_something(self, _mocked):
    do_something()
like image 6
pymen Avatar answered Oct 23 '22 16:10

pymen