1

I'm trying to create a genericized data driven test handler that I can call from my nose tests. My test file would look like:

import ScenarioHandler

def test_foo():
    scenario = ScenarioHandler(__test_foo, [1, 2])
    scenario.run()

def __test_foo(var):
    assert var % 2 == 0, 'Odd!'

ScenarioHandler would be something like this:

class ScenarioHandler(object):

    def __init__(self, test, args):
        self.test = test
        self.args = args

    def run(self):
        for arg in self.args:
            yield self.test, arg

The problem I'm running in to is that I can't figure out how to bubble the generator from ScenarioHandler.run() back up to nose. I've tried returning the generator from run() in test_foo(), and that was no good either. Is this even possible?

glglgl
  • 89,107
  • 13
  • 149
  • 217
Silas Ray
  • 25,682
  • 5
  • 48
  • 63
  • related: [Python unittest: Generate multiple tests programmatically?](http://stackoverflow.com/q/2798956/4279) – jfs Feb 07 '12 at 17:21

2 Answers2

0

Why don't you use something like:

def test_foo():
    for var in [1, 2]:
        yield __test_foo, var

See also add_tests(generator) function from my answer.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • This is a simplified version of what I'm trying to do to illustrate the thing I'm having a problem with. The whole point is to get away from calling the yield statement directly in the test so that I can have a generic handler that will do things like loop over n-length m-depth argument sets and allow me to specify ranges or lists of tests to run by number in a scenario set. I can put all that logic in each test, but that's duplication of code I want to avoid. – Silas Ray Feb 07 '12 at 17:48
0

I don't know how this nose stuff works exactly.

But your given example cannot work.

You have the call tree

test_foo() -> run().

As run() returns a generator which doesn't start until iteration starts, you should do so.

Either do

def test_foo():
    scenario = ScenarioHandler(__test_foo, [1, 2])
    for f, a in scenario.run():
        f(a)

or better, as nose provides this already by explicitly testing the flags of the given function with func.func_code.co_flags & CO_GENERATOR != 0,

def test_foo():
    scenario = ScenarioHandler(__test_foo, [1, 2])
    for f, a in scenario.run():
        yield f, a

in order to have nose make these calls.

(There could have been an even shorter thing, i. e. to use

test_meth = ScenarioHandler(__test_foo, [1, 2]).run

which, alas, doesn't work because methods are obviously not correctly detected as valid call targets.)

But you could add a "test maker" to your class.

# as method:
    def make_test(self):
        def test_generator():
            for item in self.run():
                yield item
        return test_generator

So you can do

test_foo = ScenarioHandler(__test_foo, [1, 2]).make_test()

But probably nose provides better ways to encapsulate your tests in classes by subclassing TestCase and TestSuite.

glglgl
  • 89,107
  • 13
  • 149
  • 217
  • the second snippet won't work with `nose`. `test_foo()` should be a generator itself, not just return it. You could replace `return scenario.run()` by `for f, a in scenario.run(): yield f, a`. It is so called `yield from scenario.run()` construction (it is not present in current Python) – jfs Feb 07 '12 at 17:30
  • So Sebastian, would I still get the benefit of both generators (branching code execution, not exploding the argument set until each test is actually run) with your generator repeater? Otherwise, I might as well just generate a list of arguments and iterate over them with a yield in the main test. – Silas Ray Feb 07 '12 at 17:51
  • @sr2222: generator doesn't generate values until you ask for them: [try it](http://ideone.com/Gy4wY) – jfs Feb 07 '12 at 19:22
  • I know that. The question is if the underlying generator would still function that way, or would complete beforehand. I would assume not, but I don't know enough of the ins and outs to be sure. I'm just going with outputting a list of args though. Too much headache for too little syntactical gain the other way. – Silas Ray Feb 07 '12 at 20:03
  • @J.F.Sebastian What is the difference? Returning a generator which yields `f, a` pairs is the same as being a generator yielding such pairs. Provided that you `def f(): yield 1` and `def g(): return f()`, there is no difference in calling `f()` or `g()` - in each case you get a generator yielding `1` and then stopping. – glglgl Feb 07 '12 at 21:19
  • I saw your example, but having a `break` in it changes everything. – glglgl Feb 07 '12 at 21:21
  • @glglgl: my example just demonstrates that the wrapper doesn't exhaust underlying generator all at once (to answer the question in the first sr2222's comment). – jfs Feb 07 '12 at 21:44
  • "What is the difference?" -- good question. CPython detects generator-functions using `yield` keyword. [`nose` doesn't work without `yield` keyword.](http://ideone.com/x4Kut) There is [pep-380](http://www.python.org/dev/peps/pep-0380/#motivation) – jfs Feb 07 '12 at 21:49
  • Of course. But, as my example shows, there is no way to detect if the generator which is returned comes from the function just called or from an indirectly called function. It is completely independent of nose. Or, at least, should be. This astonished me; I'll really have to fetch `nose` myself and play with it... – glglgl Feb 08 '12 at 08:07
  • 1
    Ok. It would make no difference, as the object returned shows the same behaviour. But `nose`explicitly tests the flags of the given function: `func.func_code.co_flags & CO_GENERATOR != 0` in `util.py`. That is the one big difference I wasn't aware of and which makes your point. I didn't read the `nose` docs and suppose that otherwise, the function is just called directly. – glglgl Feb 08 '12 at 08:41
  • @glglgl That is the most to the point and illuminating piece of info in this question page. Thanks. – Silas Ray Feb 09 '12 at 14:25
  • @sr2222 Then a ✓ would be nice ;-) – glglgl Feb 09 '12 at 14:34
  • @glglgl I great commented it... is there another way? – Silas Ray Feb 10 '12 at 19:48
  • Clicking on the ✓ on the left side of your favorite answer gives points to you as well as to the answerer. – glglgl Feb 10 '12 at 20:26
  • Well, I hope people from the future read all the comments then. :) – Silas Ray Feb 13 '12 at 14:53