python / cpython

The Python programming language
https://www.python.org
Other
63.55k stars 30.45k forks source link

Mocked value at method level not overriding class-level mock in unittest #122726

Open matahho opened 3 months ago

matahho commented 3 months ago

Bug report

Bug description:

Description

I encountered an issue where mocking a value at the method level does not override the class-level mock in unit tests. Even though the method-level mock should take precedence, the value from the class-level mock is still being used.

Steps to Reproduce

Create a test class with a class-level mock using @mock.patch. Add a method-level mock to one of the test methods to override the class-level mock. Run the tests and observe that the class-level mock value is still used instead of the method-level mock.

Expected Behavior

The method-level mock should override the class-level mock, and the value set in the method-level mock should be used in the test.

Actual Behavior

The class-level mock value is used, and the method-level mock is ignored.

Sample code

import unittest
from unittest import mock

# Function to be mocked
def get_value():
    return "original"

# Class-level mock
@mock.patch('module.get_value', return_value='class-level')
class TestMock(unittest.TestCase):

    def test_class_level_mock(self, mock_get_value):
        # This should return 'class-level'
        result = get_value()
        self.assertEqual(result, 'class-level')

    @mock.patch('module.get_value', return_value='method-level')
    def test_method_level_mock(self, mock_get_value):
        # This should return 'method-level', but it returns 'class-level'
        result = get_value()
        self.assertEqual(result, 'method-level')

if __name__ == '__main__':
    unittest.main()

CPython versions tested on:

3.10

Operating systems tested on:

No response

blhsing commented 3 months ago

This is because a method decorator is applied before a namespace that includes the method can be created for the class to be created, only after which the class decorator can be applied, so your method is effectively decorated like:

@mock.patch('module.get_value', return_value='class-level')
@mock.patch('module.get_value', return_value='method-level')
def test_method_level_mock(self, mock_get_value):
    ...

with a bottom-up order of application per the documentation.

Since the class decorator is applied last, the return value specified there overrides the one specified by the method decorator.