Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to unpack fixtures from parametrize_with_cases? #201

Closed
last-partizan opened this issue Apr 16, 2021 · 13 comments
Closed

How to unpack fixtures from parametrize_with_cases? #201

last-partizan opened this issue Apr 16, 2021 · 13 comments

Comments

@last-partizan
Copy link

I want to use pytest-cases with pytest-drf, which makes testing DRF looks like this:

class TestHelloWorld(
    APIViewTest,
    UsesPostMethod,
    Returns200,
):
    @pytest.fixture
    def url(self):
        return reverse('hello-world')

    @pytest.fixture
    def data(self):
        return {"param": "value"}

but, actual check test_it_returns_200 is inside Returns200 class, and i have no way to parametrize it with parametrize_with_cases.

Well, i can do this:

import pytest
from pytest_cases import parametrize_with_cases, fixture
from pytest_drf import APIViewTest, UsesPostMethod
from django.http import HttpResponse

class Cases:

    def case_one(self):
        return "/path/", {"param": "value"}


class TestView(APIViewTest, UsesPostMethod):

    def test(self, response: HttpResponse):
        assert response.status_code == 404
        assert response.request["PATH_INFO"] == "/path/"
    
    @pytest.fixture
    def url(self, urlf):
        return urlf

    @pytest.fixture
    def data(self, dataf):
        return dataf

    @fixture(unpack_into="urlf,dataf")
    @parametrize_with_cases("case", Cases)
    def _(self, case):
        return case

but this is too much boilerplate. Any ideas how to simplify this?

Ideal variant would be something like this:

@parametrize_with_cases("url,data", Cases)
class TestView(APIViewTest, UsesPostMethod):

    def test(self, response: HttpResponse):
        assert response.status_code == 404
        assert response.request["PATH_INFO"] == "/path/"

but it throws error ValueError: parameter 'url' not found in test function signature 'TestView()'

@smarie
Copy link
Owner

smarie commented May 4, 2021

Thanks for reporting @last-partizan and sorry for the delay in answering ! I'll investigate

@smarie
Copy link
Owner

smarie commented May 7, 2021

it throws error ValueError: parameter 'url' not found in test function signature 'TestView()'

Indeed applying @parametrize_with_cases to a whole class is not supported as of today. Indeed the way @parametrize_with_cases works is that it wraps the decorated function with a modified-signature function. Until this changes (see major refactoring changelist #170 that may not be soon), then you'll have to find workaround, maybe with helper functions.

The workaround that you provide seems very fine to me, I think that you can simplify it further with unpacking directly into url,data (will it work ? not sure but it should. Maybe "unpack fixture" is not good for playing inside class containers, we should look at this)

Also why do you have to recopy the http response test ? Does the test embedded in Returns200 or Returns404 not "see" the updated fixtures ?

@last-partizan
Copy link
Author

@smarie thanks for investigation :)

Unpacking directly into url,data does not work, becouse pytest_drf declares it's own url and data fixtures in APIViewTest, and they are not overriden by unpacking.

I copied http resonse test just for clarity here.

@smarie
Copy link
Owner

smarie commented May 10, 2021

they are not overriden by unpacking

This is probably because my current implementation does not unpack them in the class but the module, and therefore the ones from the parent class take precedence. I need to investigate this

@smarie
Copy link
Owner

smarie commented May 12, 2021

I confirm: unpack into creates the corresponding fixtures in the module.

I had a quick look whether I could fix it by using .common_others.get_class_that_defined_method(fixture_func) to determine the class and create the unpacked fixtures there, but it seems that I cannot do it at the time where I create the main fixture to unpack, since this happens when the @fixture decorator is called and therefore the wrapper class does not exist for python yet.

The only solution I can imagine would be to mark the created fixture with a special symbol, or to register it somewhere, and to do the unpacking later, probably in a pytest hook.

This would require a good knowledge of pytest hooks to do it in an elegant way (doing it right when the module is collected would seem relevant so that the fixtures exist "fast", directly after the module is collected, and they can therefore be used normally by pytest in the subsequent phases.

I have to admit that my current bandwidth is not compliant with such investigation. If you would be able to provide a working example of pytest hook where a fixture is late-constructed at collection phase without harm for the tests using it, I would be happy to integrate it in the right place to fix this ! Thanks again for spotting this @last-partizan

@last-partizan
Copy link
Author

Probably, such deep dive into pytest is not the right way to fix this, and i don't know pytest internals to do this.

Maybe this could be solved some other way, like this.

class Foo:
    a, b = unpack_fixture("a,b", c)

I'll take a look at it later.

@smarie
Copy link
Owner

smarie commented May 12, 2021

Oh yes of course, this should do the trick !

But note that for the same reason, this would not be "clean": a and b would be defined in the class (because you assign the output to variables that are in the class), and also in the module (because unpack_fixture performs the same dirty hack than its equivalent in the @fixture decorator: it finds the parent module and adds the unpacked fixtures to it).

Anyway, if it works then the workaround would still be better than nothing

@smarie
Copy link
Owner

smarie commented May 21, 2021

@last-partizan did it do the trick eventually ?

@last-partizan
Copy link
Author

@smarie no, it didn't.

from pytest_cases import fixture, parametrize_with_cases, unpack_fixture
from pytest_drf import APIViewTest, Returns404, UsesPostMethod


class Cases:

    def case_one(self):
        return "/", {"param": "value"}

@fixture
@parametrize_with_cases("case", Cases)
def case(case):
    return case


class TestAPIView(APIViewTest, UsesPostMethod, Returns404):
    url, data = unpack_fixture("url, data", case)

Results in

request = <SubRequest 'url' for <Function test_it_returns_404>>, kwargs = {}
source_fixture_value = <test_project.test_pytest_drf.TestAPIView object at 0x7f67baef5280>

    @pytest_fixture(name=argname, scope=scope, autouse=False, hook=hook)
    @with_signature("%s(%s, request)" % (argname, source_f_name))
    def _param_fixture(request, **kwargs):
        # ignore the "not used" marks, like in @ignore_unused
        if not is_used_request(request):
            return NOT_USED
        # get the required fixture's value (the tuple to unpack)
        source_fixture_value = kwargs.pop(source_f_name)
        # unpack: get the item at the right position.
>       return source_fixture_value[_value_idx]
E       TypeError: 'TestAPIView' object is not subscriptable

@smarie
Copy link
Owner

smarie commented May 27, 2021

Ok thanks @last-partizan , the issue is the same as for the other : when unpack_fixture is called it does not know that it is called inside a class. So it creates fixtures that do not have the self first positional argument. Therefore the case argument is not detected by pytest as a ficture dependency: it receives self (an instance of TestAPIView), hence the error message.

I will add an in_cls optional argument so that setting it to True will not register the created fixtures in the encompassing module, and the self arg will be prepended.

@smarie smarie closed this as completed in 64e2dbf May 31, 2021
@smarie
Copy link
Owner

smarie commented Jun 1, 2021

Hi @last-partizan , there is a new in_cls argument in https://smarie.github.io/python-pytest-cases/api_reference/#unpack_fixture .

import pytest
from pytest_cases import unpack_fixture, fixture

@fixture
@pytest.mark.parametrize("o", ['hello', 'world'])
def c(o):
    return o, o[0]

class TestClass:
    a, b = unpack_fixture("a,b", c, in_cls=True)

    def test_function(self, a, b):
        assert a[0] == b

Can you please check that it fixes your issue ? (use version 3.6.0+)

@smarie smarie reopened this Jun 1, 2021
@last-partizan
Copy link
Author

Yes, it works as expected. Thank you!

@smarie
Copy link
Owner

smarie commented Jun 1, 2021

Cool ! Thanks for the quick feedback @last-partizan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants