Package: megamock
MegaMock-The Developer Experience Upgrade for Python MockingPew pew! Sane defaults for mocking behavior! Patch objects, variables, attributes, etc by passing in the thing in question, rather than passing in dot-delimited path strings! Create tests faster than ever!Supported Python Versions: 3.10+New! Use the official GPT!An OpenAI ChatGPT+ GPT has been created for MegaMock! Ask questions about library usage or have it generate tests! The GPT has been coached on test generation and should outperform vanilla GPT-4. The GPT is available at:https://chat.openai.com/g/g-DtZiDVQsz-python-megamock-test-generation-assistantIf the GPT can't find info or gives bad answers, try asking it to consult the reference file. See this example:https://chat.openai.com/share/b942b5cc-bfa5-4844-8c46-922a3c2c99fcThe GPT is experimental. It has a tendency to regress during conversations. If it starts mixing MegaMock and mock, remind it to double check that its following its instructions. Its been coached on a specific pytest style but you can tell it do something else. It tends to be aggressive with the mocking, so you'll need to reel it in and use your judgement on what ultimately makes sense to mock. Don't mock something you don't need to. Don't create unit tests using mocks that will just shadow required integration tests.InstallationPip installation:pip install megamockpoetry(as a development dependency):poetry add megamock --group=devWhy Use MegaMock? (short version)MegaMock is a library that provides a better interface for mocking and patching in Python. Its version
of patch doesn't have any gotchas based on how you import something, and it also automatically
creates mocks using best practices. Additionally, the generated mock types are unions of the mocked object
andMegaMock, allowing you to better leverage your IDE's autocomplete.Mock:class_mock=mock.create_autospec(ClassICareAbout,instance=True)# cmd / alt clicking on "method_call" doesn't direct you to the definitionclass_mock.method_call.return_value="some value"# can't simply cmd / alt click and go to ClassICareAboutwithmock.patch("some.hard.to.remember.and.long.dot.path.ClassICareAbout",class_mock)asmock_instance:do_something()MegaMock:# cmd / alt clicking on ClassICareAbout takes you to the definitionpatch=MegaPatch.it(ClassICareAbout)mock_instance=patch.megainstance# cmd / alt clicking on "method_call" directs you to the definitionmock_instance.method_call.return_value="some value"do_something()Why Use MegaMock? (long version)MegaMock was created to address some shortcomings in the built-in Python library:Legacy code holds back "best practice" defaults, so many developers write sub-optimal mocks
that allow things that should not be allowed. Likewise, writing better mocks are more work,
so there's a tendency to write simpler code because, at that point in time, the developer
felt that is all that was needed. MegaMock's simple interface provides sane defaults.mock.patchis very commonly used, and can work well whenautospec=True, but has the drawback that
you need to pass in a string to the thing that is being patched. Most (all?) IDEs do not properly
recognize these strings as references to the objects that are being patched, and thus automated
refactoring and reference finding skips them. Likewise, automatically getting a dot referenced path
to an object is also commonly missing functionality. This all adds additional burden to the developer.
WithMegaPatch, you can import an object as you normally would into the test, then pass in thing
itself you want to patch. This even works for methods, attributes, and nested classes! Additionally, your IDE's autocomplete for attributes
will work in many situations as well!mock.patchhas a gotcha where the string you provide must match where the reference lives.
So, for example, if you have inmy_module.py:from other_module import Thing, then doingmock.patch("other_module.Thing")won't actually work, because the reference inmy_modulestill
points to the original. You can work around this by doingimport other_moduleand referencingThingbyother_module.Thing. MegaMock does not have this problem, and it doesn't matter how you import.FeaturesSee thefull features list.Example UsageProduction Codefrommodule.submoduleimportMyClassToMockdefmy_method(...):...a_thing=MyClassToMock(...)do_something_with_a_thing(a_thing)...defdo_something_with_a_thing(a_thing:MyClassToMock)->None:result=a_thing.some_method(...)ifresult=="a value":...Test CodefrommegamockimportMegaPatchfrommodule.submoduleimportMyClassToMockdeftest_something(...):patch=MegaPatch.it(MyClassToMock.some_method)patch.return_value="a value"my_method(...)DocumentationUsage (pytest)Withpytest, MegaMock is easily leveraged by using the included pytest plugin. You can use it by adding-p megamock.plugins.pytestto the command line.Command line example:pytest -p megamock.plugins.pytestpyproject.tomlexample:[tool.pytest.ini_options]addopts="-p megamock.plugins.pytest"The pytest plugin also automatically stopsMegaPatches after each test. Ifpytest-mockis installed, the default mocker will be switched to thepytest-mockmocker.Usage (other test frameworks)If you're not using the pytest plugin, import and execution order is important for MegaMock. When running tests, you will need to execute thestart_import_modfunction prior to importing any production or test code. You will also want it so the loader is not used in production.Core ClassesMegaMock- the primary class for a mocked object. This is similar toMagicMock. To create a mock instance of a class, useMegaMock.it(MyClass)to makeMyClassthespec. To create a mock class (the type) useMegaMock.the_class(MyClass). To create mock instances of instantiated objects, functions, etc, useMegaMock.this(some_object).MegaPatch- the class for patching. This is similar topatch. UseMegaPath.it(MyObject)to replace new instances of theMyObjectclass.Mega- helper class for accessing mock attributes without having to memorize them due to lost type inference. UseMega(some_megamock).
Note that theassert_methods, such asassert_called_once, is nowcalled_onceand returns a boolean. The assertion error
is stored inMega.last_assertion_error. This is typically for doing asserts against mocked functions and methods.Dependency injection example:frommegamockimportMegaMockdeftest_something(...):manager=MegaMock.it(MyManagerClass)service=SomeService(manager)...MegaPatch example:fromelsewhereimportServicefrommegamockimportMegaPatchdeftest_something(...):patched=MegaPatch.it(Service.make_external_call)patched.return_value=SomeValue(...)service=SomeService(...)code_under_test(service)...MegaMockobjects have the same attributes as regularMagicMocks plusmegamockandmegainstance.
For example,my_mega_mock.megamock.spyis the object being spied, if set.my_class_mock.megainstanceis the instance returned when the class is instantiated. Note that you typically access the megainstance withMegaMock(my_class_mock).megainstancedue to limitations in the type system.Theguidance documentis available to provide in-depth information on using mocking and MegaMock. Continuing reading to
quickly jump in to examples.Learning By ExampleAll examples below have the following imports:frommy_moduleimportMyClassfrommegamockimportMega,MegaMock,MegaPatchCreating a mock instance of a class:mock_instance=MegaMock.it(MyClass)Creating a mock class itself:mock_class=MegaMock.the_class(MyClass)func_that_wants_a_type(mock_class)Spying an object:my_thing=MyClass()spied_class=MegaMock.this(spy=my_thing)# ... do stuff with spied_class...Mega(spied_class.some_method).call_args_list# same as wraps# check whether a value was accessed# if things aren't as expected, you can pull up the debugger and see the stack tracesassertlen(spied_class.megamock.spied_access["some_attribute"])==1spy_access_list=spied_class.megamock.spied_access["some_attribute"]spy_access:SpyAccess=spy_access_list[0]spy_access.attr_value# shallow copy of what was returnedspy_access.stacktrace# where the access happenedspy_access.time# when it happened (from time.time())spy_access.top_of_stacktrace# a shorthand property intended to be used when debugging in the IDEspy_access.format_stacktrace()# return a list of strings for the stacktracespy_access.print_stacktrace()# display the stacktrace to the consolePatching a class:mock_patch=MegaPatch.it(MyClass)# the class itselfmock_patch.new_value# the class instancemock_patch.megainstance# the return value of the __call__ method on the classmock_patch.megainstance.return_valuePatching a class attribute:# temporarily update the max retries to 0mega_patch=MegaPatch.it(MyClass.max_retries,new=0)Patching a class method:mega_patch=MegaPatch.it(MyClass.my_method,return_value=...)Alternatively:mega_patch=MegaPatch.it(MyClass.my_method)mega_patch.mock.return_value=...mega_patch=MegaPatch.it(MyClass.my_method)mega_patch.new_value.return_value=...You can also alter the return value of your mock without creating a separate mock object first.mega_patch.return_value.user=SomeUser()Working withMegaPatchand classes:mega_patch.new_valueis the classtypeitselfmega_patch=MegaPatch.it(MyClass)mega_patch.new_value.xisMyClass.xmega_patch.return_valueis the classinstancereturned. However, there is the propertymegainstancewhich is preferred because it has better type hinting.mega_patch=MegaPatch.it(MyClass)# instead of this, for which the static type is Any:mega_patch.return_valueisMyClass()# use this, which has a static type of MegaMock | MyClass:mega_patch.megainstanceisMyClass()Patching a module attribute:importmy_moduleMegaPatch.it(my_module.some_attribute,new=...)Patching a method of a nested class:importmy_moduleMegaPatch.it(my_module.MyClass.MyNestedClass.some_method,return_value=...)Setting the return value:my_mock.my_method.return_value="foo"Turning on real logic:importmy_modulemega_patch=MegaPatch.it(my_module.SomeClass)Mega(mega_patch.megainstance.some_pure_logic_method).use_real_logic()do_something_that_invokes_that_function(...)Congrats on Reading This Far! Here's an Art Gallery!Nobody said it was a big art gallery. Feel free to submit a PR that helps fix that. No artistic skill required.