-
Notifications
You must be signed in to change notification settings - Fork 5
Mocking properties #7
Comments
# cat.py
from sounds import play_sound
class Cat:
def meow(self):
play_sound('meow')
return True # sounds.py
import midi
def play_sound(sound):
return midi.play(sound) Ok great. It's working! Now to write some tests: from cat import Cat
def test_cat_can_meow(self, mocker):
cat = Cat()
result = cat.meow()
assert result Ok, but now I'm in trouble because we don't have midi support in our unit tests, so I have to mock the from cat import Cat
def test_cat_can_meow(self, mocker):
mocker.patch('sounds.play_sound', lambda self: 0)
cat = Cat()
result = cat.meow()
assert result But wait! My patch didn't work.... Oh now I see it.... because Aha! I'll be sneaky... # cat.py
class Cat:
def meow(self):
from sounds import play_sound
play_sound('meow')
return True Hold on a second! Now we've created all sorts of problems for ourselves. For one, it's confusing. Anyone who's casually browsing your Let's just all agree that we should stop doing this. And Here's what I'm talking about: # cat.py
class Cat:
def __init__(self, play_sound):
self.play_sound = play_sound
def meow(self):
self.play_sound('meow')
return True Or better yet, make the constructor take an entire object that encapsulates all the functionality in the sounds module: # cat.py
from sounds import SoundPlayer
class Cat:
def __init__(self, sounds: SoundPlayer):
self.sounds = play_sound
def meow(self):
self.sounds.play_sound('meow')
return True We can easily test this using good ol' from cat import Cat
def test_cat_can_meow(self, mocker):
cat = Cat(MagicMock())
result = cat.meow()
assert result Problem solved. No patching necessary. It's allowed to have a factory method on the # cat.py
from sounds import SoundPlayer
class Cat:
def __init__(self, sounds: SoundPlayer):
self.sounds = play_sound
def meow(self):
self.sounds.play_sound('meow')
return True
@classmethod
def factory(cls):
return cls(SoundPlayer.factory()) # See what I did there ? ;) Anyway, this is something we should be doing more of... |
from cat import Cat
def test_cat_can_meow(self, mocker):
mocker.patch('sounds.play_sound', lambda self: 0)
cat = Cat()
result = cat.meow()
assert result this is covered in the docs: https://docs.python.org/3/library/unittest.mock.html#where-to-patch to patch it correctly you need to patch where its used: from cat import Cat
def test_cat_can_meow(self, mocker):
mocker.patch('cat.play_sound', lambda self: 0)
cat = Cat()
result = cat.meow()
assert result there are other problems with the test above, but I don't think the usage of |
Having to know where stuff is looked up and when is another layer of mental overhead that contributes to tech debt over time. We have to remember that our code base will be touched by hundreds of people, and they're not all going to be willing (or able) to spend the cycles to figure out how to surgically patch your code. I think there's abundant evidence of this in our code as it stands right now. Dependency injection is foolproof, robust, and it won't break. It has other advantages too -- the other day I was running a jupyter notebook that called a method that writes to the database. Since my class allowed for dependency injection, I could easily swap in a read-only version of the dependency. |
👍 lets reduce the number of patches by having our objects depend on the stuff they need, and require it at initialization |
I’m all for it. Isn’t this the way the style guide suggests? Should we have another bad example for |
I agree with @adambom. I think Important functionalityWhat is patching doing? Inherently, it is saying "I'm not testing this." So we're writing a test and we're specifically excluding things from our test. How do we know that what we've patched isn't so critical to the purpose of the method that by patching it, we'd made our test useless? def important_method(param):
result_1 = _complex_calculation_1(param)
result_2 = _complex_calculation_2(result_1)
...
result_N = _complex_calculation_N(result_N_minus_1)
return result_N * 2 And I'll see a test that patches Some methods are so large and complex that they can't be unit tested without a lot of mocking, but I think mocking is just a way of glossing over a method that needs to be refactored such that its parts can be tested without mocking. Dependency injection avoids this because it allows the code itself to tell you what implementation details are important and necessary to the method and which ones can be outsourced, meaning that you can still test the method if you change those details. In the Locking implementation detailsI've seen this test I don't know how many times: def my_public_method(data):
result = _my_inner_method(data)
return result
class TestMyPublicMethod:
def test_my_inner_method_is_called(self, mocker)
patched_inner_method = mocker.patch('_my_inner_method')
my_public_method({})
patched_inner_method.assert_called_once() This isn't just a useless test. This is a test that actively prevents what might be a beneficial refactor. It eliminates the benefit of encapsulation. The point of encapsulation is that callers to Of course we can avoid this through testing standards, but I would argue that the best testing standard to adopt here is to treat every patch with suspicion: why is it necessary to patch here? The only really good reason that comes to mind for patching is to avoid integrations, such as DB calls. But even then, it would be much better to develop service boundaries/dependency injection such that test environments run with mock services that make patching unnecessary.
It is a problem here because it locks us into the |
I think there should be a section on how |
I believe this discussion about |
Discussion on best practice of Mocking properties.
The text was updated successfully, but these errors were encountered: