Giter Club home page Giter Club logo

dingus's Introduction

DINGUSES

A dingus is sort of like a mock object. The main difference is that you don't set up expectations ahead of time. You just run your code, using a dingus in place of another object or class, and it will record what happens to it. Then, once your code has been exercised, you can make assertions about what it did to the dingus.

A new dingus is created from the Dingus class. You can give dinguses names, which helps with debugging your tests, especially when there are multiple dinguses in play.

>>> from dingus import Dingus
>>> d = Dingus('root')
>>> d
<Dingus root>

Accessing any attribute of a dingus will return a new dingus.

>>> d.something
<Dingus root.something>

There are a few exceptions for special dingus methods. We'll see some in a bit.

A dingus can also be called like a function or method. It doesn't care how many arguments you give it or what those arguments are. Calls to a dingus will always return the same object, regardless of the arguments.

>>> d()
<Dingus root()>
>>> d('argument')
<Dingus root()>
>>> d(55)
<Dingus root()>

RECORDING AND ASSERTIONS

At any time we can get a list of calls that have been made to a dingus. Each entry in the call list contains:

  • the name of the method called (or "()" if the dingus itself was called)
  • The arguments, or () if none
  • The keyword arguments, or {} if none
  • The value that was returned to the caller

Here is a list of the calls we've made to d so far:

>>> from pprint import pprint
>>> pprint(d.calls)
[('()', (), {}, <Dingus root()>),
 ('()', ('argument',), {}, <Dingus root()>),
 ('()', (55,), {}, <Dingus root()>)]

You can filter calls by name, arguments, and keyword arguments:

>>> pprint(d.calls('()', 55))
[('()', (55,), {}, <Dingus root()>)]

If you don't care about a particular argument's value, you can use the value DontCare when filtering:

>>> from dingus import DontCare
>>> pprint(d.calls('()', DontCare))
[('()', ('argument',), {}, <Dingus root()>),
 ('()', (55,), {}, <Dingus root()>)]

Dinguses can do more than just have attributes accessed and be called. They support many Python operators. The goal is to allow, and record, any interaction:

>>> d = Dingus('root')
>>> (2 ** d.something)['hello']() / 100 * 'foo'
<Dingus root.something.__rpow__[hello]().__div__.__mul__>

(Hopefully your real-world dingus recordings won't look like this!)

PATCHING

Dingus provides a context manager for patching objects during tests. For example:

>>> from dingus import patch
>>> import urllib2
>>> with patch('urllib2.urlopen'):
...     print urllib2.urlopen.__class__
<class 'dingus.Dingus'>
>>> print urllib2.urlopen.__class__
<type 'function'>

You can also use this as a decorator on your test methods:

>>> @patch('urllib2.urlopen')
... def test_something(self):
...     pass
...

ISOLATION

The opposite of patch is isolate. It patches everything except the named object:

>>> from dingus import isolate
>>> @isolate('urllib2.urlparse')
... def test_urlparse(self):
...     pass
...

When this test runs, everything in the urllib2 module except urlparse will be a dingus. Note that this may be slow to execute if the module contains many objects; performance patches are welcome. :)

DANGEROUS MAGIC

Dingus can also automatically replace a module's globals when running tests. This allows you to write fully isolated unit tests. See examples/urllib2/test_urllib2.py for an example. The author no longer recommends this feature, as it can encourage very brittle tests. You should feel the pain of manually mocking dependencies; the pain will tell you when a class collaborates with too many others.

dingus's People

Contributors

abyx avatar anttih avatar chromy avatar dstanek avatar gaconnet avatar garybernhardt avatar jnrowe avatar poiati avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

dingus's Issues

Clarify licensing

Hi there,

could you please clarify the license for dingus? Setup.py states MIT whereas dingus.py says BSD (but which BSD version). We would like to package it for openSUSE but we need a valid licensing statement. Thanks!

Unable to install dingus from repo or pypi

$ sudo python3 setup.py install
Traceback (most recent call last):
  File "setup.py", line 6, in <module>
    long_description=file('README.txt').read(),
NameError: name 'file' is not defined
$ sudo pip3 install dingus
Collecting dingus
  Downloading dingus-0.3.4.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-90owktsy/dingus/setup.py", line 6, in <module>
        long_description=file('README.txt').read(),
    NameError: name 'file' is not defined

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-90owktsy/dingus/
$ python3 --version
Python 3.5.1+
$ pip3 --version
pip 8.1.2 from /usr/local/lib/python3.5/dist-packages (python 3.5)

`isolate` does not work with submodules

isolate can isolate b in a.b but not c in a.b.c.

class WhenIsolatingSubmoduleObjects:
    def should_isolate(self):
        @isolate("os.path.isdir")
        def ensure_isolation():
            assert not isinstance(os.path.isdir, Dingus)
            assert isinstance(os.path.isfile, Dingus)

        assert not isinstance(os.path.isfile, Dingus)
        ensure_isolation()
        assert not isinstance(os.path.isfile, Dingus)

"Namespaces are one honking great idea -- let's do more of those!" :-)

Can't return a value for __getitem__

Imported from BitBucket:

Reported by Anonymous, created 6 months ago.

It doesn't seem possible to have a return element for getitem. For example:

from dingus import Dingus

d = Dingus(__getitem____returns='foo')
print d['bar'] # expects 'foo' but you get the initial dingus

Another option is to try this:

d = Dingus(**{"['bar']__returns": 'foo'})
d['bar']
This ends up adding a getitem call to the call list.

Gary Bernhardt / garybernhardt

Wow, good catch. This is because getitem is an actual method, not just another dingus like most attributes. I'm at a bit of a loss about how to make this work without introducing a ton of internally confusing complexity. I think that the getattr function would have to return a special dingus that, when called, logs the special method name in the parent and does the normal getattr stuff.

That last bit is important: dinguses act as perfectly valid dictionaries, and that needs to remain true.

Fortunately, that means you can temporarily work around your problem by doing:

>>> d = Dingus()
>>> d['foo'] = 'bar'
>>> print d['foo']
bar

You could also monkey patch a new getattr onto the dingus if you really need it to return a specific value regardless of how it's called. Ugly, I know, but I'm definitely not going to have time to fix this soon. :( Clever patches are encouraged, of course! :)

Gary Bernhardt / garybernhardt

Actually, if you need a dingus to return a value regardless of the getitem key, a better short-term solution would be to just subclass Dingus and override getitem to return the value.
My allergy to inheritance sometimes keeps me from seeing it when it really is the solution you want! ;)

Support for recording PEP 234 style iterators

Imported from BitBucket:

Reported by Anonymous, created 6 months ago.

Specifically the syntax that replaces the has_key() method on dictionaries.

In [41]: d.has_key('str') Out[41]: <Dingus dingus_15795344.has_key()>
In [42]: 'str' in d Out[42]: False
In [43]: d.calls Out[43]: [('has_key', ('str',), {}, <Dingus dingus_15795344.has_key()>)]

http://www.python.org/dev/peps/pep-0234/

I may be missing something, but it seems like iterators that use the in keyword are not recorded, I haven't looked into implementing this myself yet, but would be happy to give it a shot.

Gary Bernhardt / garybernhardt

It looks like we just never defined the contains method. Patches are always welcome! :) I'll probably get to this eventually, but it hasn't been an itch I've had just yet. :)

flzz

Added attachment dingus_issue_11.patch.
Not sure if this covers the bases appropriately, but its working for my needs at the moment :)

Anonymous

Is it likely that this patch will be merged in?

assert_call seems not work on patched object

Hi,

I was interested to see how it works.
After patching the original is not called anymore and I can see the calls. Fine.
Two issues I've found:

  • the one is already mentioned by another guy: patched("...") as var
  • the one issue I found is that it seems the assertion don't work.
    I provided therefore the whole test code I have used for it.
    I think since you always intend to provide Dingus objects (right?) and
    you did write a test where you exactly did those assertion tests ...
    I assume then that it also should work for my case:
from dingus import Dingus, patch
from pprint import pprint

def low_foo(*args, **kwargs):
    print("low_foo(%s, %s)" % (args, kwargs))

def high_foo(*args, **kwargs):
    print("high_foo(%s, %s)" % (args, kwargs))
    low_foo(*args, **kwargs)

high_foo(1, 3.1415, [1,2,3], name="hello")

print("now patching ...")
with patch("__main__.low_foo"):
    high_foo(1, 3.1415, [1,2,3], name="hello")
    print(low_foo)
    pprint(low_foo.calls())
    low_foo.assert_call(1, 3.1415, [1,2,3], name="hella")

class Bar(object):
    def __init__(self, *args, **kwargs):
        print("Bar.__init__(%s, %s)" % (args, kwargs))
    def test(self, *args, **kwargs):
        print("Bar.test(%s, %s)" % (args, kwargs))

class Foo(object):
    def __init__(self, *args, **kwargs):
        print("Foo.__init__(%s, %s)" % (args, kwargs))
        self.bar = Bar(*args, **kwargs)
    def test(self, *args, **kwargs):
        print("Foo.test(%s, %s)" % (args, kwargs))
        self.bar.test(*args, **kwargs)

foo = Foo("hello world", value=123)
foo.test("sunny days", value=321)

print("now patching ...")
with patch("__main__.Bar"):
    foo = Foo("hello world", value=123)
    foo.test("sunny days", value=321)
    print(Bar)
    pprint(Bar.calls())
    Bar.test.assert_call("sunny days", values=322)

Upload a new version to the cheeseshop!

Hey Gary,

I realized I started using the patch function from the repo, but we haven't seen a new release for quite some time. Mind pushing a 0.3 to pypi?

Thanks!

DingusTestCase should replace file, open, etc.

Imported from BitBucket:

Reported by Gary Bernhardt / garybernhardt, created about a year ago.

Off the top of my head, this should include file, open, exec, and execfile. There may be more. Check the builtin module.

Ryan Freckleton / ryan_freckleton

Looking through builtin it looks like the following should be mocked (they talk to the 'outside' either through the file system or stdin):
file
eval
open
execfile
input
raw_input
There are also a couple functions associated with the import machinery, import and reload, but I can't think of how to mock those in a way that will work correctly all of the time.

Gary Bernhardt / garybernhardt

Thanks for looking through the builtins, Ryan! That will make it much easier to add them to DingusTestCase. I'm much less worried about import and reload than the others you listed. If classes and functions under test are importing code within themselves, there are probably bigger problems in the system than having well-stubbed unit tests. :)
Spam

DingusTestCase accepts str for exclude parameter

dingus.DingusTestCase accepts a string for the exclude parameter, but such usage is probably not going to do what the user expects. I encountered code that called DingusTestCase(auth.identify, 'NoActiveVersionError'). What the user clearly intended was to exclude the auth.NoActiveVersionError from being made a Dingus. Instead, what NoActiveVersionError is still a dingus and any attributes in the set(['N', 'o', 'A', 'c',..., 'r']) were excluded.

I suggest adding:

if isinstance(exclude, basestring):
 raise ValueError("String not allowed for exclude - to exclude a string of characters, use tuple(characters)")

or

if isinstance(exclude, basestring):
  exclude = [exclude]

Either option would probably follow the principle of 'least surprise'.

`patch` will not "patch" nonexistent objects.

patch raises AttributeError if you try to patch a name that does not exist.

class WhenPatchingMissingObjects:
    @patch('urllib2.missing_object')
    def should_create_object_with_dingus(self):
        assert isinstance(urllib2.missing_object, Dingus)

    def should_delete_object_after_patched_function_exits(self):
        @patch('urllib2.missing_object')
        def patch_urllib2():
            pass
        assert not hasattr(urllib2, 'missing_object')
        patch_urllib2()
        assert not hasattr(urllib2, 'missing_object')

Although failing in this case makes sense if you interpret the word "patch" strictly, I found myself wishing I could do this when patching a library dependency that exposed new names as a way to support multiple deployed versions. E.g. pilate.thrwow_him_to_the_floor and pilate.thwow_him_to_the_floor2, where the newer 2 function is not yet ready for pushing & "pip"ing so I need to stub it in all of the separate projects that want to upgrade their usage.

Does this package support Python 3?

On 19 February 2017, Python 3 will be 3,000 days old! We are interested to see if we can get at least 50% of the Top 5,000 PyPI packages to compatible with Python 3 by that date. We are really close and given that this package is in the PyPI Top 5,000, we seek your assistance in pushing us over that threshold.

So, if this package already supports Python 3 then please consider adding an appropriate trove classifier "Programming Language :: Python :: 3" to this package's PyPI page so that tools can automaticly determine its Python 3 compatibility.

returner() doesn't record method calls

Imported from BitBucket:

Reported by Gary Bernhardt / garybernhardt, created 6 months ago.

A method created with returns= records its calls:
>>> d = Dingus(x__returns=5)
>>> d.x()
5
>>> d.calls
[('x', (), {}, 5)]
but a method created with returner() doesn't:
>>> d = Dingus(x=returner(5))
>>> d.x()
5
>>> d.calls
[]

deepcopy loses attributes

Example:

>>> d = Dingus('d', foo=1)
>>> d.foo
1
>>> deepcopy(d).foo
<Dingus d.__deepcopy__().foo>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.