6

I apologize in advance for repeating the word parameter 1000 times. My use case is as follows.

I'm using pytest to test a parser that parses fields out of product pages from an online shop.

I have parametrized a fixture, so that each fixture imports data for one product. By data, I mean the HTML source code and a list of fields with their expected values. Down the line I have a parametrized test that takes a list of tuples (field, expected value), so that each field gets its own test.

Basically, the "bare bones" problem would like something like this:

from pytest import fixture, mark

products = [
    {
    'text': 'bla bla', 
    'fields': [('bla', 0), ('foo', -1)]
    },
    {
    'text': 'foo bar', 
    'fields': [('bla', -1), ('foo', 0), ('bar', 4)]
    }
]

@fixture(params=products)
def product(request):
    return request.param


@mark.parametrize('field_key, field_value', product['fields'])
def test_parser(product, field_key, field_value):
    assert product['text'].find(field_key) == field_value

In the context of the @mark.parametrize decorator, product isn't interpreted as a fixture, so pytest returns:

TypeError: 'function' object has no attribute '__getitem__'

There's lot of introspection magic going on with pytest and I'm having trouble finding a solution. I had a look at this question but this is not quite what I want. Is there a way to achieve this? Thanks.

Community
  • 1
  • 1
cyberbikepunk
  • 1,302
  • 11
  • 14

2 Answers2

2

I don't think you need fixtures to achieve your goal.

Given your example data, this is a possible way:

from pytest import mark


products = [
    {
        'text': 'bla bla',
        'fields': [('bla', 0), ('foo', -1)]
    },
    {
        'text': 'foo bar',
        'fields': [('bla', -1), ('foo', 0), ('bar', 4)]
    }
]

possible_params = []
for product in products:  # Iterate over the products to build all desired invocations
    for field_key, field_value in product['fields']:
        possible_params.append((product['text'], field_key, field_value))

@mark.parametrize('text,field_key,field_value', possible_params)
def test_parser(text, field_key, field_value):
    assert text.find(field_key) == field_value
silviot
  • 4,615
  • 5
  • 38
  • 51
1

I was able to get this to run (in Python3), although the tests fail:

#!python3
from pytest import fixture, mark

Products = [
    {
        'text':   'bla bla',
        'fields': [('bla', 0), ('foo', -1)]
    },
    {
        'text':   'foo bar',
        'fields': [('bla', -1), ('foo', 0), ('bar', 1)]
    }
]

@fixture(scope="module",
        params=[(prod['text'], *tpl) for prod in Products for tpl in prod['fields']])
def product(request):
    return request.param


def test_parser(product):
    haystack,needle,index = product
    assert haystack.find(needle) == index
aghast
  • 14,785
  • 3
  • 24
  • 56
  • Thanks @AustinHastings. Basically, the idea is to let the fixture to do all the iteration work. Incidentally, you could also write the script the other way around, so that the parametrized test does all the work. [See the gist](https://gist.github.com/cyberbikepunk/9640a3585d1400cb9a9dc4a5130edb46). I'm kind of disappointed, though that I cannot use information from a fixture to create a parametrized test. I'm still curious to know if this is possible, or alternatively, why this is not desirable. – cyberbikepunk May 10 '16 at 08:10
  • Incidentally, I've edited the script so that tests should now all be greenie greenie (although it's not really the point) – cyberbikepunk May 10 '16 at 08:56
  • 1
    I think it's an information problem. Specifying `params=[]` is probably enough information to indicate that there needs to be a loop. But there's a bunch of activity in the background with generating name-strings that involve the loop data, etc., that I'm not surprised only one level is supported. You could probably explode your test cases and get it to work that way. – aghast May 10 '16 at 16:15