Pixie
Pixie

Reputation: 311

Python testing a call to a class's method instantiated as a varable in another class

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

Answers (2)

Stefan Marinov
Stefan Marinov

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

MrBean Bremen
MrBean Bremen

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

Related Questions