-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
Description
Bug report
Concurrent calls to NonCallableMock.__getattr__() result in a race condition.
NonCallableMock implements __getattr__(), which instantiates a new mock object for an attribute if it doesn't exist yet when it's accessed. However, if the code under test is multi-threaded and multiple threads try to access the same attribute, each thread may call __getattr__() separately and instantiate a different mock object representing the same attribute.
I created a minimal reproducible example here: https://github.com/noah-weingarden/unittest.mock-race-condition
The test and "code-under-test" are both in mre.py. events.py contains two threading.Event objects used to synchronize access to the mock objects. I copy-pasted the Python 3.10 unittest.mock module into mock.py and added code which forces a specific order of events. I also added logging so that it's clear what's going on. Search "CHANGES START" to see where my modifications are. Even though the code-under-test calls sendall(), this specific order of events always creates two different MagicMock instances for sendall(), causing the test to fail because it doesn't see that sendall() was called.
Run python3 mre.py to observe the logs and test failure. If you remove my synchronization, the test will fail non-deterministically as opposed to every time.
Here's the order of events:
test_message_sent()tries to accesssocket().__enter__().sendall, which doesn't exist, so__getattr__()is called on the mock forsocket().__enter__().__getattr__()sees that no mock object forsocket().__enter__().sendallexists, so it calls_get_child_mock().- Before
_get_child_mock()runs, the context switches to the thread runningmain(). main()callssendall().socket().__enter__()still doesn't have asendallattribute, so__getattr__()and then_get_child_mock()are called._get_child_mock()creates a newMagicMockand sets it as the mock object forsendall(). Nowmain()has successfully calledsendall()and its call is recorded for this mock object.- The context switches back to
test_message_sent(), which continues running_get_child_mock(). It creates a newMagicMockand overwrites the earlier one. - Now when the test retrieves
socket().__enter__().sendall, it's accessing a different mock object than the onemain()used. It has no way to tell thatmain()actually made a call.
Your environment
At a minimum, this race condition can occur on any version of Python between 3.6 and 3.10.
Motivation for solving
Mocking is frequently used to test multi-threaded programs, so it's important that unittest.mock be thread-safe. It's not intuitive that I would need to protect code for mocking in tests from race conditions. I spent months trying to hunt down a race condition within my organization's code before I realized that the issue was our tests' interactions with unittest.mock.
Suggested solution
Synchronize access to NonCallableMock.__getattr__() with a mutex. Either create one lock shared by all NonCallableMock objects and acquire it during each call to __getattr__(), or use fine-grained locking by creating one for each top-level NonCallableMock object or even every NonCallableMock object. Mocking is used almost exclusively in unit tests, so I believe the performance hit from synchronization would be worth it. I have a solution along these lines and would be happy to contribute it.