Better way to mock class attribute in python unit test

67,476

Solution 1

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}):

Solution 2

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

with mock.patch.object(Base, 'assignment', {'bucket': 'head'}):
   # do stuff

Solution 3

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.

Solution 4

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

Solution 5

Here is an example how to unit-test your Base class:

  • mocking multiple class attributes of different types (ie: dict and int)
  • using the @patch decorator and pytest framework with with python 2.7+ or 3+.

# -*- coding: utf-8 -*-
try: #python 3
    from unittest.mock import patch, PropertyMock
except ImportError as e: #python 2
    from mock import patch, PropertyMock 

from base import Base

@patch('base.Base.assign_dict', new_callable=PropertyMock, return_value=dict(a=1, b=2, c=3))
@patch('base.Base.assign_int',  new_callable=PropertyMock, return_value=9765)
def test_type(mock_dict, mock_int):
    """Test if mocked class attributes have correct types"""
    assert isinstance(Base().assign_dict, dict)
    assert isinstance(Base().assign_int , int)
Share:
67,476

Related videos on Youtube

Ivo van der Wijk
Author by

Ivo van der Wijk

Python and wannabe Android coder

Updated on June 14, 2020

Comments

  • Ivo van der Wijk
    Ivo van der Wijk almost 4 years

    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?

  • Ivo van der Wijk
    Ivo van der Wijk about 10 years
    Bar.assignment.__get__ = lambda: {1:1} wouldn't have worked here (just tried), so mock injects/mocks a descriptor. Also, mock takes care of restoring the 'old' definition which avoids nasty side effects when modifying globally this way. I can do some old school hacking around like you suggest (and I use to) but I want to learn the 'mock' way :)
  • Martijn Pieters
    Martijn Pieters about 10 years
    @IvovanderWijk: With Bar.assignment being a mock?
  • Martijn Pieters
    Martijn Pieters about 10 years
    @IvovanderWijk: I am surprised that PropertyMock didn't work though.
  • Ivo van der Wijk
    Ivo van der Wijk about 10 years
    Either by partially mocking Bar or by only mocking the 'assignment' attribute, whatever the mock module provides.
  • Ivo van der Wijk
    Ivo van der Wijk about 10 years
    new_callable is a good suggestion. PropertyMock(return_value={'a':1}) makes it even better :) (no need for the 'as a' or further assignment anymore)
  • Martijn Pieters
    Martijn Pieters about 10 years
    @IvovanderWijk: as for Bar.assignment.__get__ = lambda: {'a': 1}; that won't work because the __get__ method would need to accept self, obj and type_ arguments.. The Mock object at least uses *args, **kwargs signatures for their callables.
  • Ivo van der Wijk
    Ivo van der Wijk about 10 years
    No, python refuses the assignment: AttributeError: 'dict' object has no attribute 'get'
  • Ivo van der Wijk
    Ivo van der Wijk about 10 years
  • Martijn Pieters
    Martijn Pieters about 10 years
    @IvovanderWijk: That'd be correct, because dict() objects do not support setting additional attributes (there is no .__dict__ attribute on dictionaries).
  • Ivo van der Wijk
    Ivo van der Wijk about 10 years
    Good point. Didn't get the decorated to work with pytest at first (it conflicted with pytest's fixture argument 'injection') but it turns out to be a matter of proper argument order (patches go first)
  • Dan Keder
    Dan Keder about 10 years
    It seems that since mock-1.0.1 it isn't an issue anymore: bitbucket.org/hpk42/pytest/issue/217/…
  • Sush
    Sush over 5 years
    Thank you so much! This answer helped me somuch!
  • LondonRob
    LondonRob over 2 years
    The third positional argument here is the new= keyword argument. It actually looks pretty good as a positional argument, but function calls with more than two positional arguments always make me feel a bit nervous....
  • LondonRob
    LondonRob over 2 years
    The fact that this works does make me think that PropertyMock might not need to exist??