classification
Title: Add restricted mocks to the python unittest mocking framework
Type: enhancement Stage: patch review
Components: Library (Lib) Versions: Python 3.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: László Kiss Kollár, berker.peksag, ericvw, grzgrzgrz3, haypo, mariocj89, michael.foord, rbcollins
Priority: normal Keywords:

Created on 2017-06-01 20:16 by mariocj89, last changed 2017-06-24 08:05 by László Kiss Kollár.

Pull Requests
URL Status Linked Edit
PR 1923 open mariocj89, 2017-06-02 19:49
Messages (9)
msg294961 - (view) Author: Mario Corchero (mariocj89) * Date: 2017-06-01 20:16
Define a way to disable the automatic generation of submocks when accessing an attribute of a mock which is not set.


Rationale:
Inspired by GMock RestrictedMock, it aims to allow the developer to declare a narrow interface to the mock that defines what the mocks allows to be called on.
The feature of mocks returning mocks by default is extremely useful but not always desired. Quite often you rely on it only at the time you are writing the test but you want it to be disabled at the time the mock is passed into your code.

It also prevents user errors when mocking incorrect paths or having typos when calling attributes/methods of the mock.
We have tried it internally in our company and it gives quite a nicer user experience for many use cases, specially for new users of mock as it helps out when you mock the wrong path.

Posible interfaces:
New Mock type, SeledMock which can be used instead of the "common" mock that has an attribute "sealed" which once set to true disables the dynamic generation of "submocks"


The final goal is to be able to write tests like:

>>> m = mock.Mock()  # or = mock.SealableMock()
>>> m.method1.return_value.attr1.method2.return_value = 1
>>> mock.seal(m)  # or mock.sealed = True

>>> m.method1().attr1.method2()  # This path has been declared above
# 1
>>> m.method1().attr2  # This was not defined so it is going to raise a meaningful exception
# Exception: SealedMockAttributeAccess: mock.method1().attr2
msg294969 - (view) Author: Mario Corchero (mariocj89) * Date: 2017-06-01 21:00
Sample implementation using the new class:
https://github.com/mariocj89/cpython/commit/2f13963159e239de041cd68273b9fc4a2aa778cd

Sample implementation using the new function to seal existing mocks:
https://github.com/mariocj89/cpython/commit/9ba039e3996f4bf357d4827123e0b570d84f5bb6

Happy to submit a PR if the idea is accepted.

The only benefit I see from the using a separate class is that you will be able to do:

>>> m = mock.SealedMock()
>>> m.important_attr = 42
>>> m.freeflow_attribute = mock.Mock()
>>> mock.seal(m)

which will allow to define "subparts of the mock" without the sealing.

That said I still prefer the function implementation as it looks much nicer (credit to Victor Stinner for the idea)
msg296159 - (view) Author: Berker Peksag (berker.peksag) * (Python committer) Date: 2017-06-16 05:04
I personally never need this feature before so I will add Michael and Robert to nosy list to take their opinions.
msg296573 - (view) Author: Grzegorz Grzywacz (grzgrzgrz3) * Date: 2017-06-21 18:45
Existing mock implementation already has that feature. Mock attributes can be limited with `spec` attribute.


>>> inner_m = Mock(spec=["method2"], **{"method2.return_value": 1})
>>> m = Mock(spec=["method1"], **{"method1.return_value": inner_m})
>>> 
>>> m.method1().method2()
1
>>> 
>>> m.method1().attr
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.5/unittest/mock.py", line 580, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'attr'
msg296589 - (view) Author: Mario Corchero (mariocj89) * Date: 2017-06-21 22:15
Whilst I agree that using spec can be used for a similar purpose and I did not know about being able to do nested definitions via the arguments (the **{"method1.return_value": 1}, really cool!) I find the idea of allowing users to halt the mock generation really useful. It is much less disruptive and feels more natural.

Compare:
>>> inner_m = Mock(spec=["method2"], **{"method2.return_value": 1})
>>> m = Mock(spec=["method1"], **{"method1.return_value": inner_m})

with: 
>>> m = mock.Mock()
>>> m.method1().method2() = 1
>>> mock.seal(m)


In brief, seal allows users to just add the method to their existing workflow where they use generic mocks. Moreover, it is extremely user friendly, many of the developers that struggle with the mocking module found seal really helpful.
msg296600 - (view) Author: Michael Foord (michael.foord) * (Python committer) Date: 2017-06-21 23:22
I don't see what this buys over spec and autospec. I'd be inclined to close it without a compelling use case beyond what is already supported.
msg296609 - (view) Author: STINNER Victor (haypo) * (Python committer) Date: 2017-06-22 00:09
> I don't see what this buys over spec and autospec. I'd be inclined to close it without a compelling use case beyond what is already supported.

I proposed to Mario to open an issue since I like his API. Even if "sealing" mocks is unlikely to be the most common case, when you need it, I prefer his API over the specs thing which reminds me bad time with mox. I prefer the declarative Python-like API, rather than Mock(spec=["method2"], **{"method2.return_value": 1}).

But yeah, technically specs and sealing seems similar. It's just another way to describe a mock. Since I prefer sealing, I would like to allow users to choose between specs and sealing.
msg296697 - (view) Author: Michael Foord (michael.foord) * (Python committer) Date: 2017-06-23 10:50
Note that you can use an object as the parameter to the spec argument rather than just a list of attributes. 

Hmmm... I'm not totally opposed to the addition of a "seal_mock" method (optionally with a recurse boolean for child mocks) being added to the Mock/MagicMock classes. It's quite a nice API. I don't like the idea of an additional Mock class for this.
msg296705 - (view) Author: STINNER Victor (haypo) * (Python committer) Date: 2017-06-23 12:34
> I don't like the idea of an additional Mock class for this.

Hum, in the current implementation, it's an enhancement of the Mock class, no more a new class.
History
Date User Action Args
2017-06-24 08:05:21László Kiss Kollársetnosy: + László Kiss Kollár
2017-06-23 12:34:02hayposetmessages: + msg296705
2017-06-23 10:50:21michael.foordsetmessages: + msg296697
2017-06-22 00:09:25hayposetmessages: + msg296609
2017-06-21 23:22:55michael.foordsetmessages: + msg296600
2017-06-21 22:15:41mariocj89setmessages: + msg296589
2017-06-21 18:45:28grzgrzgrz3setnosy: + grzgrzgrz3
messages: + msg296573
2017-06-16 05:04:36berker.peksagsetnosy: + rbcollins, berker.peksag, michael.foord

messages: + msg296159
stage: patch review
2017-06-02 19:49:19mariocj89setpull_requests: + pull_request2003
2017-06-01 21:49:56ericvwsetnosy: + ericvw
2017-06-01 21:00:59mariocj89setmessages: + msg294969
2017-06-01 20:16:56mariocj89create