1

I have a situation where I'm trying to modify the arguments passed to a decorator on one of my class methods. The code looks something like this:

class MyClass(object):
  @tryagain(retries=3)
  def mymethod(self, arg):
    ... do stuff ...

My problem is I'd like to alter the "retries" variable to something less than 3 when running my unit tests, but keep it at "3" for the production code. Unfortunately, it doesn't look like I can do something like this:

  @tryagain(retries=self.retries)
  def mymethod(self, arg):
    ... do stuff ...

or

  @tryagain(retries=MyClass.retries)
  def mymethod(self, arg):
    ... do stuff ...

because the class isn't defined at the point the arguments are passed to the decorator (as near as I can tell).

I also tried to add the variable within the module like so:

retries = 1
def MyClass(object):
    @tryagain(retries=retries)
    def mymethod(self, arg):
      ... do stuff ...

but then I can't seem to modify the value of "retries" from within my unit tests. Is there another way to accomplish what I'm trying to do?

2 Answers 2

1

I assume you try to reduce the number of retrials to increase test speed.

If so, modifying the number of retries variable doesn't seem to be the best approach. Instead, you could unit test the function mymethod without decorator first, and then create a mock function of mymethod. Let's call it mock_mymethod, decorate it with @tryagain and test if the logic of `tryagain actually works.

Check the mock module to see how to create a mock instance, this article about mock is also worth reading.

Sign up to request clarification or add additional context in comments.

4 Comments

Yes, that's correct. I guess my next question would be, how do I test mymethod without the decorator? It looks like I can patch the decorator, but it's not all that straightforward. I'll have to read up on it.
Try mock tryagain and set its return value as the same input function, then test mymethod. That's practically without any decoration effects. After that, mock mymethod and test tryagain.
If you're serious about unittest, mock is definitely worth to learn, and I think it's well suited to your need in this particular case.
Yeah, I'd started using mock for the first time on this project, so I'm a little familiar, but there are still things I'm learning. I was finally able to figure out where to properly patch the decorator after some work last night, and I think that's the way to go.
1

You could use an environment variable, set from your calling code (it might be good to put a default in here

import os
# ...
class MyClass(object):
    @tryagain(retries=int(os.environ['project_num_retries']))
    def mymethod(self, arg):
        print("mymethod")

Or use a "globals"-type module, for example: project_settings.py containing:

num_retries = 3

Then

import project_settings

class MyClass(object):
    @tryagain(retries=project_settings.num_retries)
    def mymethod(self, arg):
        print("mymethod")

But I'm not sure decorating your code with test information is how you really should go about it -- what about:

class MyClass(object):
    def mymethod(self, arg):
        print("mymethod")

Then in something like unittests.py:

DEV_TESTS = True  # Change to False for production
num_retries = 3 if not DEV_TESTS else 1

import <your class>
class UnitTests():
    def __init__(self):
        self.c = <your_class>.MyClass()

    @tryagain(retries=num_retries)
    def test_mymethod(self):
        self.c.mymethod("Foo")

t = UnitTests()
t.test_mymethod()

If you were so inclined, this unittests.py could be used with something like python's unittest package with:

DEV_TESTS = True  # Change to False for production
num_retries = 3 if not DEV_TESTS else 1

import unittest
import <your class>
class UnitTests(unittest.TestCase):
    def setUp(self):
        self.c = <your class>.MyClass()

    @tryagain(retries=num_retries)
    def test_mymethod(self):
        self.c.mymethod("Foo")

Note, I used the following simple example of a @tryagain decorator, yours may be more complicated and require some tuning of the examples:

def tryagain(retries):
    def wrap(f):
        def wrapped_f(*args,**kwargs):
            for _ in xrange(retries):
                f(*args,**kwargs)
        return wrapped_f
    return wrap

2 Comments

I think setting the environmental variable will do what I want. Maybe it was unclear, but I'm trying to get around (or shorten) the time it takes for me to run my unit tests. I want @tryagain to decorate the production code, because I want "mymethod" to be called again if it fails the first time. I do not want it firing off when running my tests, because when testing certain fail cases, it takes a long time to come back (it has a backoff time).
Oh, so @tryagain isn't a test decorator, but a production decorator that you want to modify the argument to when you're testing -- understood. Ignore the second part of the answer then.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.