1

I want to check that my function has no side-effects, or only side-effects affecting precise variables. Is there a function to check that it actually has no side-effects (or side-effects on only certain variables)?

If not, how can I go about writing my own as follows:

My idea would be something like this, initialising, calling the function under test, and then calling the final method:

class test_side_effects(parents_scope, exclude_variables=[]):
    def __init__():
        for variable_name, variable_initial in parents_scope.items():
            if variable_name not in exclude_variables:
                setattr(self, "test_"+variable_name, variable_initial)
    def final(self, final_parents_scope):
        for variable_name, variable_final in final_parents_scope.items():
            if variable_name[:5] is "test_" and variable_name not in exclude_variables:
                assert getattr(self, "test_"+variable_name) is variable_final, "Unexpected side effect of %s from %s to %s" % (variable_name, variable_initial, variable_final)

#here parents_scope should be inputted as dict(globals(),**locals())

I'm unsure if this is precisely the dictionary I want...

Finally, should I be doing this? If not, why not?

Community
  • 1
  • 1
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
  • You can't dynamically create variables by trying to assign to a string, and even if you could, it wouldn't be a good idea. – Wooble Jul 19 '12 at 11:03
  • @Wooble I realise I can't do it like this, but thought perhaps there was a way around this bit. Why would it not be a good idea? *I could enclose it in a testing-wrapper so that ordinarily it wasn't called so memory etc. wouldn't be a problem...* – Andy Hayden Jul 19 '12 at 11:06
  • @Wooble I think I can set up a test-object which could [set these dynamically using setattr](http://stackoverflow.com/questions/8831270/creating-a-variable-name-dynamically). – Andy Hayden Jul 19 '12 at 11:17
  • I don't see your problem here. If you follow the common rule to not modify global objects and only change objects which are submitted as function parameters there is no need for such a test. – schlamar Jul 19 '12 at 11:57
  • @ms4py Legacy code. It's a good rule, but unfortunately people haven't always followed it... I think it would be good to include this as part of a testing suite. :) – Andy Hayden Jul 19 '12 at 12:04
  • You could use [`mock` library to test for side-effects](http://docs.python.org/dev/library/unittest.mock.html). – jfs Jul 19 '12 at 12:09
  • @J.F.Sebastian I know that you can produce side-effects with a mock object, but how can you test for side-effects. Please elaborate. – schlamar Jul 19 '12 at 12:13
  • @ms4py: mock an object, call tested function, check that the mock object is in an expected state. – jfs Jul 19 '12 at 12:18
  • @hayden As you are talking about legacy code I think it is Python 2? Because in Python 3 there would be the `nonlocal` statement for such a task (accessing the parent scope). – schlamar Jul 19 '12 at 12:24
  • I think refactoring the code would be easier than getting this to work :) @J.F.Sebastian Yes, but you cannot mock an object in the parent scope (see the comment in the answer below). – schlamar Jul 19 '12 at 12:26

1 Answers1

3

I'm not familiar with the nested function testing library that you might be writing a test with, but it seems like you should really be using classes here (i.e. TestCase in many frameworks).

If your question then, is relating to getting the parent variables in your TestCase, you could get the __dict__ (It wasn't clear to me what the "Parent" variables you were referring to.

UPDATE: @hayden posted a gist to show the use of parent variables:

def f():
    a = 2
    b = 1
    def g():
        #a = 3
        b = 2
        c = 1
        print dict(globals(), **locals()) #prints a=1, but we want a=2 (from f)
    g()
a = 1
f()

If this is converted to a dictionary, then the problem is solvable with:

class f(object): # could be unittest TestCase
    def setUp(self, a=2, b=1):
        self.a = a
        self.b = b

    def g(self):
        #a = 3
        b = 2
        c = 1
        full_scope = globals().copy()
        full_scope.update(self.__dict__)
        full_scope.update(locals())
        full_scope.pop('full_scope')
        print full_scope # print a = 1

my_test = f()
my_test.setUp(a=1)
my_test.g()

You are right to look for a tool which has already implemented this. I am hopeful that somebody else will have an already implemented solution.

pelson
  • 21,252
  • 4
  • 92
  • 99
  • I wrote [this gist](https://gist.github.com/3143331) which I think explains the parent scope issue. I have updated my code above to reflect this... Is this feature available in any testing frameworks?? – Andy Hayden Jul 19 '12 at 11:57
  • I have updated my answer based on the gist. As I said, I don't know of an already implemented solution, but I am hopeful somebody else will. – pelson Jul 19 '12 at 12:16
  • To me (naively) it seems like something that ought to be in some test library... The whole scope thing is tricky to implement (to get my head around!) – Andy Hayden Jul 19 '12 at 12:39