Home > Enterprise >  Which method is the best way to mock classes used in the `__init__` of a class being tested?
Which method is the best way to mock classes used in the `__init__` of a class being tested?

Time:02-01

I was hoping that someone could give me guidance on some of the approaches I've tried for mocking. I'm really trying to understand what the best method is for this general case (I think general enough). And whether or not there are better approaches or if my current approaches need some tweaking.

This is my current setup. I have three classes that are instantiated in the class being tested. These classes have methods that I need to create this function I'm testing.

class Foo:
   def get_foo(self): # Method to be used in a Generator method
      ...

class Bar:
   def get_bar(self): # Method to be used in a Generator method
      ...

class Baz:
   def get_baz(self): # Method to be used in a Generator method
      ...

# Class being tested
class Generator:
   def __init__(self):
      self.foo = Foo()
      self.bar = Bar()
      self.baz = Baz()
   
   ...rest of Generator...

I am trying to mock the classes under __init__ in class Generator before I create an instance of Generator. I want to do this because classes like Foo and Bar call mongoDB to fetch some data. I don't want it to actually spin up a mongo instance during testing, so I'm trying to mock it out before that happens.

The approaches I am listing below have been successful in my case. But like I said before, I'm really trying to understand what the best approach is.

Approach 1

I believe this approaches the goal correctly. It would mock everything before instantiated, but it seems inefficient to keep doing return_value.get_value..... I have not been able to find a simplified version of this that has actually worked.

class TestGenerator(unittest.TestCase):
   @patch('generatorModule.Foo')
   @patch('generatorModule.Bar')
   @patch('generatorModule.Baz')
   def setUpClass(cls, patchFoo, patchBar, patchBaz):
      cls.foo = patchFoo.return_value
      cls.foo.get_foo.return_value.get_value.return_value = 'Some string I want to return'

      cls.bar = patchBar.return_value
      cls.bar.get_bar.return_value.get_value.return_value = 'Another string I want to return'

      cls.baz = patchBaz.return_value
      cls.baz.get_baz.return_value.get_value.return_value = 'Another another string I want to return'

      cls.generator = Generator()

      ...rest of test...

Approach 2

I found this one through some research into mocking. It essentially returns None for the __init__ function in Generator. Then I can mock the attributes I want using Magic Mock. This also works, but it seems like a cop-out setting everything from scratch for each test. It works, and I understand how it works, but I don't know if this is a valid solution. I'm not sure what problems may occur if I use this approach for all of my tests going forward.

class TestGenerator(unittest.TestCase):
   @patch.object(GeneratClass, "__init__", Mock(return_value=None))
   def setUpClass(cls):
      mockFoo = MagicMock()
      mockFoo.get_foo.return_value = 'Some string I want to return'

      mockBar = MagicMock()
      mockBar.get_bar.return_value = 'Another string I want to return'

      mockBaz = MagicMock()
      mockBaz.get_baz.return_value = 'Another another string I want to return'

      cls.generator = Generator()
      cls.generator.foo = mockFoo
      cls.generator.bar = mockBar
      cls.generator.baz = mockBaz

      ...rest of test...

Any notes on my current approaches, or what might be the best approach would be amazing. The version of Python that I am using is 3.9.6. Thank you for any help you can give!

CodePudding user response:

Have you thought about using dependency injection here?
Instead of initializing Foo, Bar and Baz Inside Generator.__init__(), you can pass instances of those classes to the function.

class Generator:
    def __init__(self, foo: Foo, bar: Bar, baz: Baz):
        self.foo = foo
        self.bar = bar
        self.baz = baz

    ...rest of Generator...

generator = Generator(Foo(), Bar(), Baz())

Then, in your test you could initialize the class using your mocks instead of the original classes.

If you don’t want to pass these instances every time you initialize Generator, you could either give the arguments a default value:

class Generator:
    def __init__(self, foo: Optional[Foo] = None, bar: Optional[Bar] = None, baz: Optional[Baz] = None):
        self.foo = foo or Foo()
        self.bar = bar or Bar()
        self.baz = baz or Baz()

    ...rest of Generator...

generator = Generator()

Or, the method I prefer, create a classmethod:

class Generator:
    def __init__(self, foo: Foo, bar: Bar, baz: Baz):
        self.foo = foo
        self.bar = bar
        self.baz = baz

    @classmethod
    def auto_initialized(cls):
        return cls(Foo(), Bar(), Baz())

    ...rest of Generator...

generator = Generator.auto_initialized()
  • Related