Home > Software design >  Python testing a call to a class's method instantiated as a varable in another class
Python testing a call to a class's method instantiated as a varable in another class

Time:03-07

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.

CodePudding user response:

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")   
  • Related