-
-
Notifications
You must be signed in to change notification settings - Fork 30.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Race condition in unittest.mock
#98624
Comments
The |
My students in EECS 485 Web Systems at the University of Michigan ran into this bug while writing a MapReduce Framework (project spec). We unit test student solutions using the Mock library and ran into this problem along the way. I can confirm that @noah-weingarden's suggested solution works for us. We monkey-patched the library. Thank you to the cpython team for taking a look at this issue, and thanks to @noah-weingarden for taking the time to narrow this down and provide a suggested fix. |
Thanks for the clarification; it makes sense to me now. |
* Added lock to NonCallableMock in unittest.mock * Add blurb * Nitpick blurb * Edit comment based on @Jason-Y-Z's review * Add link to GH issue
…8688) * Added lock to NonCallableMock in unittest.mock * Add blurb * Nitpick blurb * Edit comment based on @Jason-Y-Z's review * Add link to GH issue (cherry picked from commit 0346edd) Co-authored-by: noah-weingarden <33741795+noah-weingarden@users.noreply.github.com>
…8688) * Added lock to NonCallableMock in unittest.mock * Add blurb * Nitpick blurb * Edit comment based on @Jason-Y-Z's review * Add link to GH issue (cherry picked from commit 0346edd) Co-authored-by: noah-weingarden <33741795+noah-weingarden@users.noreply.github.com>
* Added lock to NonCallableMock in unittest.mock * Add blurb * Nitpick blurb * Edit comment based on @Jason-Y-Z's review * Add link to GH issue
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 twothreading.Event
objects used to synchronize access to the mock objects. I copy-pasted the Python 3.10unittest.mock
module intomock.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 callssendall()
, this specific order of events always creates two differentMagicMock
instances forsendall()
, causing the test to fail because it doesn't see thatsendall()
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__().sendall
exists, so it calls_get_child_mock()
._get_child_mock()
runs, the context switches to the thread runningmain()
.main()
callssendall()
.socket().__enter__()
still doesn't have asendall
attribute, so__getattr__()
and then_get_child_mock()
are called._get_child_mock()
creates a newMagicMock
and sets it as the mock object forsendall()
. Nowmain()
has successfully calledsendall()
and its call is recorded for this mock object.test_message_sent()
, which continues running_get_child_mock()
. It creates a newMagicMock
and overwrites the earlier one.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 withunittest.mock
.Suggested solution
Synchronize access to
NonCallableMock.__getattr__()
with a mutex. Either create one lock shared by allNonCallableMock
objects and acquire it during each call to__getattr__()
, or use fine-grained locking by creating one for each top-levelNonCallableMock
object or even everyNonCallableMock
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.The text was updated successfully, but these errors were encountered: