Reputation: 311
I have a class A
, which receives config for another class B in its constructor. A
then creates a variable out of B
by using this config and creating an instance of class B
.
Later in the code it calls this variable as self.b.do_somthing()
.
How can I make sure this method call is actually called with mock/patch? Below I post a super simple dummy code doing a similar thing. I want to test that perform()
actually called sing_a_song()
Please pay attention to the comments in the code
my_app.py
class Sing:
def __init__(self, **config):
self.songs = {
"almost home": """
Almost home
Brother, it won't be long
Soon all your burdens will be gone
With all your strength""",
"smooth criminal": """
Annie, are you okay?
So, Annie, are you okay? Are you okay, Annie?
"""
}
self.config = config
def sing_a_song(self, song_name):
return self.songs.get(song_name.lower())
class Singer:
def __init__(self, song_to_sing):
self.song_to_sing = song_to_sing
self.sing = Sing(**some_config_that_doesnt_matter) # <-- this part is the culprit
def perform(self):
if self.song_to_sing.lower() == "smooth criminal":
self.sing.sing_a_song(self.song_to_sing) # <-- and it's called here
else:
return "I don't know this song, not gonna perform."
And the test (which doesn't work)
test.py
import unittest
from unittest.mock import patch
from my_app import Singer
class TestStringMethods(unittest.TestCase):
@patch("my_app.Sing", autospec=True)
def test_download(self, sing_mock):
melissa = Singer("smooth criminal")
melissa.perform()
sing_mock.assert_called() # <-- works (because of the call inside the constructor or bc of the call inside perform() ???
sing_mock.sing_a_song.assert_called() # <-- returns AssertionError: Expected 'sing_a_song' to have been called.
sing_mock.assert_called_with("smooth criminal") # <-- returns AssertionError: Expected call: Sing('smooth criminal') ... Actual call: Sing()
# If I do the following, the test passes, BUT if i after that REMOVE the actual
# call to self.sing.sing_a_song(self.song_to_sing), it still passes ??!?!!?
sing_mock.sing_a_song("smooth criminal")
if __name__ == "__main__":
unittest.main()
Also.. i realize that the call is actually made to the class and not the object's method as if the method is static, but it's not defined as such and therefore I somehow need to fix this.
So.. what am I doing wrong, please help. I am still new the mocking and patching and I am quite confused no matter how many articles I read.
Upvotes: 3
Views: 1786
Reputation: 581
The other approach works for mocking the whole class Sing
. However, depending on what you want to test exactly, you may want to mock only the specific method:
class TestStringMethods(unittest.TestCase):
@patch('my_app.Sing.sing_a_song')
def test_download(self, sing_a_song_mock):
melissa = Singer('smooth criminal')
melissa.perform()
sing_a_song_mock.assert_called() # this works
sing_a_song_mock.assert_called_with('smooth criminal') # works, too
For instance, this might be useful in some cases when you already know that you tested the instantiation of the class in other tests, and you now just want to test the exact call of the class method in the current test.
Upvotes: 2
Reputation: 16805
If you are mocking a class, and want to check method calls on that class, you have to check them on the instance of the class. You get the class instance of a mock using return_value
on the mock (which generally gives you the result of the call operator, which in the case of a class is the class instantiation). Note that instantiating a specific class mock will always give you the same "instance" (e.g another mock).
So in your case, the following will work:
class TestStringMethods(unittest.TestCase):
@patch("my_app.Sing", autospec=True)
def test_download(self, sing_mock):
melissa = Singer("smooth criminal")
melissa.perform()
sing_mock.assert_called() # this asserts that the class has been instantiated
sing_mock_instance = sing_mock.return_value # split off for readability
sing_mock_instance.sing_a_song.assert_called()
sing_mock_instance.sing_a_song.assert_called_with("smooth criminal")
Upvotes: 2