Package: automock
There are some things that need to be mocked in unit tests.For example: API clients for other backend services - we don’t want to run an
instance of the other service just for our unit tests. The other service will
have its own tests and we only want to test that our code confiorms to the API
contract of the other service. Similarly for 3rd party services - we don’t want
our unit tests to connect out over the internet to talk to the 3rd party service
(even if they offer a ‘sandbox’ test environment) for the same reasons as above,
and because this is a recipe for flaky tests.(There is certainly a role for integration tests which do make live calls to
other services, but the bulk of tests won’t be this kind and need mocking).Python has the excellentmocklibrary to help with this.However, say you have six API clients for backend services which are used
extensively in many places in code for your mobile app backend. You’re going to
end up with a big ‘stack’ of patch decorators on many tests, e.g.:@mock.patch('services.users.client.get_user',return_value=MockUser(id=1))@mock.patch('services.products.client.get_product',return_value=MockProduct(id=1))@mock.patch('services.paypal.client.make_payment',return_value=PaypalResult('success'))deftest_some_web_view(self,*mocks)Say you have thousands of unit tests, these decorators need applying to many of
them. Every time you write a new test you’ll need to remember to patch things.Enterautomock.Basically we want some functions to be ‘mocked by default’ when running tests.
But we also need to be able to easily replace the default mocks in some cases,
where the test needs a specific return value.automockmakes this easy-ish.UsagepipinstallautomockIntroductionThe key idea is that we define a ‘mock factory’ for each function we want to be
automocked. When called without arguments the factory should return a suitable
‘default’ mock that will allow most tests to pass. The default mock factory is
justMagicMockfrom themocklibrary.Registering a function to be mocked is simple:importautomockautomock.register('services.users.client.get_user')By default this provides aMagicMockand is equivalent to decoratingallyour test cases with:@mock.patch('services.users.client.get_user')(for more scenarios seeCustomising mock factoriesbelow)For this to work you just need to do two things.You need to ensure that the modules containingautomock.registercalls get imported before the tests run. To achieve this we have anAUTOMOCK_REGISTRATION_IMPORTSconfig setting. This should contain string paths
to modules containing registration calls, e.g.:AUTOMOCK_REGISTRATION_IMPORTS=('services.users.test_mocks','services.products.test_mocks','services.paypal.test_mocks',)If you’re running your tests underpytestthen you don’t need to do anything else - Automock registers a pytest plugin
(namedautomockin pytest) that ensures your test cases all run patched.If you’re running under another test-runner then your test cases need to inherit
from one of our helper classes, e.g.:fromautomockimportAutomockTestCase,AutomockTestCaseMixinclassTestWebViews(AutomockTestCase):...classTestSpecialViews(AutomockTestCaseMixin,MyCustomTestCase):...This will ensure the mock patches get applied before the tests run, and stopped
afterwards.Alternatively you can start/stop patching manually:fromunittestimportTestCaseimportautomockclassTestStuff(TestCase):# as a decorator@automock.activate()deftest_stuff(self):# automocks active...# as a context-managerdeftest_other_stuff(self):# automocks inactive...withautomock.activate():# automocks active...# automocks inactiveConfigurationSettings are intended to be configured primarily via a python file, such
as your existing Djangosettings.py. To bootstrap this, there are a couple
ofenv varsto control how config is loaded:AUTOMOCK_APP_CONFIGshould be an import path to a python module, for example:AUTOMOCK_APP_CONFIG=django.conf.settingsAUTOMOCK_CONFIG_NAMESPACESets the prefix used for loading further config values from env and
config file. Defaults toAUTOMOCK.The following config keys are available (and are prefixed withAUTOMOCK_by default, seeAUTOMOCK_CONFIG_NAMESPACEabove):<namespace>_REGISTRATION_IMPORTSlist of import paths to modules
containingautomock.registercallsPatching and importsAnimportant point to noteabout the path you mock:This has the same caveats as when usingmock.patchdirectly. Namely that
you must patch the pathwhere it is imported.For example if you do:# mypackage/mymodule.pyfromservices.product.clientimportget_productWhen you patch it:# won't work:patch('services.product.client.get_product')# works:patch('mypackage.mymodule.get_product')DON’T DO THIS (see thisblog postfor more details).This import style will cause us problems if we want to mock-by-default all
usages of a particular function, because we only register a single path to mock.Instead you need to use one of the following import styleseverywherein your
codebase that the function to mocked is used:# mypackage/mymodule.py# eitherfromservices.productimportclientasproduct_clientproduct_client.get_product(*args)# orimportservices.product.clientasproduct_clientproduct_client.get_product(*args)This will ensure that we can:automock.register('services.product.client.get_product')and have that work reliably.NOTE:Alwaysimport automockand use asautomock.registerto ensure there is
only one registry active.Customising mock factoriesIt’s likely you need to do more than provide a bareMagicMock. For example
we might want to customise the response based on some values from the request.Inmock.Mockthis is achieved via a ‘side effect’. So we might want to
define our mock factory like this:defbatch_counters_mock(return_value=None,side_effect=None,*args,**kwargs):ifreturn_valueisNoneandside_effectisNone:defside_effect(product_ids,*args,**kwargs):return{str(p_id):0forp_idinproduct_ids}returnmock.MagicMock(return_value=return_value,side_effect=side_effect,*args,**kwargs)automock.register('services.products.client.batch_counters',batch_counters_mock)Note that we passed the custom mock factory as second argument toregister.As an alternative we can use decorator syntax:@automock.register('services.products.client.batch_counters')defbatch_counters_mock(return_value=None,side_effect=None,*args,**kwargs):ifreturn_valueisNoneandside_effectisNone:defside_effect(product_ids,*args,**kwargs):return{str(p_id):0forp_idinproduct_ids}returnmock.MagicMock(return_value=return_value,side_effect=side_effect,*args,**kwargs)Now in our tests we can:importservices.products.clientasproducts_clientdeftest_counters():counters=products_client.batch_counters([1,2])# we got a default value for each of the ids we passed in:assertcounters=={'1':0,'2':0}(This is a useless test of course, it’s just to demonstrate the mocking)Okay. What if we need a custom return value for a particular test?Well, firstly the regularmock.patchstill works, you could apply that in
your test case.Automock also provides aswap_mockhelper that allows us to take advantage
of our custom mock factory.Let’s say our factory looks like:@automock.register('services.things.client.do_something')defdo_something_mock(success=True):ifsuccess:returnmock.MagicMock(return_value='OK')else:returnmock.MagicMock(side_effect=requests.HTTPError())In our tests we can:importpytestimportrequestsfromautomockimportswap_mockimportservices.things.clientasthings_clientdeftest_success():# default mock from factory gives success responseassertthings_client.do_something()=='OK'@swap_mock('services.things.client.do_something',success=False)deftest_fail():# swap mock applies a customised mock from our factorywithpytest.raises(requests.HTPPError):things_client.do_something()What happened here is that the*args, **kwargsfrom ourswap_mockcall
are passed through to thedo_something_mocktoget a new mockwhich is
then applied in place of the default.We can also use this as a context manager:importpytestimportrequestsfromautomockimportswap_mockimportservices.things.clientasthings_clientdeftest_do_something():assertthings_client.do_something()=='OK'withswap_mock('services.things.client.do_something',success=False):withpytest.raises(requests.HTPPError):things_client.do_something()assertthings_client.do_something()=='OK'Checking mocked callsIt’s common in tests to want to check if a mocked function was called, and
with correct arguments etc. If you usemock.patchdirectly this is easy
because it returns the mock object to you.Automock provides theget_mockhelper to achieve the same thing:fromautomockimportget_mockimportservices.things.clientasthings_clientdeftest_success():assertthings_client.do_something()=='OK'mocked=get_mock('services.things.client.do_something')assertmocked.calledTesting the automocked functionsOk, so you’ve mocked your API clients or whatever. How do you test the mocked
functions themselves if they’re mocked out everywhere?Firstly, you could just not inherit fromAutomockTestCasein those tests.But maybe you have a bunch of other automocks you want to keep in place still.Automock provides anunmockhelper:importpytestimportresponsesfromautomockimportunmockimportservices.things.clientasthings_client@responses.activate@unmock('services.things.client.do_something')deftest_do_something_not_found():responses.add(responses.GET,'https://thingservice.ourcompany.com/api/1/something',json={'error':'Not Found'},status=404)withpytest.raises(requests.HTPPError):things_client.do_something()(for functions which make HTTP calls we recommend the excellentresponseslibrary)Here we have un-mocked our client method so that we can test that it correctly
handles a 404 response from the remote service.CompatibilityThis project is tested against:Python 2.7Python 3.6Python 3.7Python 3.8Running the testsCircleCIThe easiest way to test the full version matrix is to install the
CircleCI command line app:https://circleci.com/docs/2.0/local-jobs/(requires Docker)The cli does not support ‘workflows’ at the moment so you have to run
the two Python version jobs separately:circlecibuild--jobpython-2.7circlecibuild--jobpython-3.6py.test (single python version)It’s also possible to run the tests locally, allowing for debugging of
errors that occur.Now decide which Python version you want to test and create a virtualenv:pyenvvirtualenv3.6.4automockpipinstall-rrequirements-test.txtNow we can run the tests:maketest