I know I'm a few years late to the party... but regarding "is there's a simpler way to get this done without using mock" and as i hat the same problem recently: This object will be quite resistant to a lot of things, is is small and doesn't use dependencies. It is a kind of "null object design pattern"
class Dummy():
    '''
    Dummy that can be called and is also a context manager.
    It will always return itself, so that you can chain calls.
    '''
    # Accessing attributes
    def __getattr__(self, name):
        return self
    # Callable
    def __call__(self, *args, **kwargs):
        return self
    # Context manager
    def __enter__(self):
        return self
    def __exit__(self, *args, **kwargs):
        pass
    # Indexing, Subscripting, Slicing
    def __getitem__(self, *args, **kwargs):
        return self
    def __setitem__(self, key, value):
        self
    # String representation
    def __str__(self) -> str:
        return '<Just a Dummy>'
It can then be used for example:
logger = Dummy()
logger.info('Black hole for logging', stacklevel=1)
open = Dummy()
with open("file.txt", "r") as f: # fake context manager
    print(f.read()) # <Just a Dummy>
mlflow = Dummy()
with mlflow.start_run(): # chaining works also
    mlflow.log_param("param1", 5)
    mlflow.a.b().c.d() # multiple chaining
# indexing and subscripting
df = Dummy()
df['col']
df[7]
df.iloc[2:42, ['col1', 'col2']]
Of course, it does not return meaningful values, this would need to be implemented for every attribute/function e.g.
    def get_count(self):
        return 0
All suggestions for improvement are welcome