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()