Writing your own mocking libary

Since I was testing different libraries I wasn't satisified by the result. I cannot yet say whether I'm able to produce better results by writing an own one but let's give it a try. This notebook document will be constantly updated if there are any news.


In [9]:
import sys
sys.path.append("..")

What you can do with a mock instance

Before patching things let us see what we can do with a mock. A mock is placeholder for something else; it can be a module a class a function or a method. For now let us just concentrate on the mock instance itself.

  • you can call a method with any parameters also the method does not exist
  • you can write to an attribute also the attribute does not exist (it will be created then)
  • you can read from an attribute also the attribute does not exist (it will be the mock instance itself then)
  • you can use the mock to store attributes.
  • the mock - of course - has predefines methods but with special signature by starting with name "mock_" (like mock_history)
  • Each call and each access to an attribute is stored in the mock history. For calls we store the name and all method arguments. For attributes we store the operation, name, current value and new value.
  • You have to be precise when searching for a call or for an attribute in the history (otherwise you have to iterate yourself)

It's to mention that the order of keyword arguments is not preserved.


In [2]:
from concept.mock import Mock, Call, Attribute

mock = Mock(1024, 3.1415926535, "hello", key1=2048, key2=1.707, key3="world")
mock.foo(4096, "another day in paradise", key1=8192)
mock.age = 99
mock.age = 100
age = mock.age

for entry in mock.mock_history():
    print(entry)

# precise call
assert Call("__init__", 1024, 3.1415926535, "hello", key1=2048, key2=1.707, key3="world") in mock.mock_history()
# not precise enough
assert Call("__init__") not in mock.mock_history()
# precise attribute
assert Attribute(operation=Attribute.READ, name="age", given_value=100) in mock.mock_history()
# not precise enough
assert Attribute(operation=Attribute.READ, name="age") not in mock.mock_history()


Call(__init__, 1024, 3.1415926535, hello, key1=2048, key2=1.707, key3=world)
Call(foo, 4096, another day in paradise, key1=8192)
Attribute(operation=created, name=age, value=99)
Attribute(operation=changed, name=age, value=99 -> 100)
Attribute(operation=read, name=age, value=100)

Mocking a module (design considerations)

At this moment the patching functionality is not yet implemented. Anyway I have to play a bit with it to get an idea about how it works and what functionality is missing. The chdir and rm are very probably the easy thing when mocking a module. Of course the init in the history is not wanted when mocking a module but that can be adjusted easily since the history is a list and we remove first item. What you can see - or to be more precise - what you can not see is the call of isfile. Since os is now a mock a new mock is provided when accessing path (because path is not known); when accessing isfile on the mock for path the call will be registered there. So here comes the one question how to handle that for patching because usually the patching mechanism usually is about one object.

Also important learning in this section: if we have not yet done the import of the os module we don't get it back with a simple substitution ... we have to remove it from the dictionary. The import mechanism works same ways as caching; things already loaded will not be loaded again.


In [20]:
mocked_module = Mock()

old_os = sys.modules['os']
sys.modules['os'] = mocked_module

import os
os.chdir('/tmp')
os.rm('/tmp/file_that_does_not_exist')
os.path.isfile('/tmp/file_that_does_not_exist')

for entry in mocked_module.mock_history():
    print(entry)

# doesn't work as expected
print(old_os)
sys.modules['os'] = old_os
print(os.path.isdir('/tmp'))
# important, otherwise the import will not happen
del sys.modules['os']
# now we are fine
import os
assert os.path.isdir('/tmp')
assert not os.path.isfile('/tmp/file_that_does_not_exist')


Call(__init__)
Call(chdir, /tmp)
Call(rm, /tmp/file_that_does_not_exist)
<module 'os' from '/usr/lib/python2.7/os.pyc'>
None

In [ ]: