Chuyển một tham số cho một hàm cố định


114

Tôi đang sử dụng py.test để kiểm tra một số mã DLL được bọc trong MyTester lớp python. Để xác thực, tôi cần ghi lại một số dữ liệu kiểm tra trong quá trình kiểm tra và xử lý thêm sau đó. Vì tôi có nhiều tệp _... thử nghiệm, tôi muốn sử dụng lại việc tạo đối tượng thử nghiệm (ví dụ của MyTester) cho hầu hết các thử nghiệm của mình.

Vì đối tượng tester là đối tượng có tham chiếu đến các biến và hàm của DLL, tôi cần chuyển danh sách các biến của DLL cho đối tượng tester cho mỗi tệp kiểm tra (các biến được ghi giống nhau đối với test_ .. . tập tin). Nội dung của danh sách sẽ được sử dụng để ghi dữ liệu đã chỉ định.

Ý tưởng của tôi là làm điều đó bằng cách nào đó như thế này:

import pytest

class MyTester():
    def __init__(self, arg = ["var0", "var1"]):
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

# located in conftest.py (because other test will reuse it)

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester()
    return _tester

# located in test_...py

# @pytest.mark.usefixtures("tester") 
class TestIt():

    # def __init__(self):
    #     self.args_for_tester = ["var1", "var2"]
    #     # how to pass this list to the tester fixture?

    def test_tc1(self, tester):
       tester.dothis()
       assert 0 # for demo purpose

    def test_tc2(self, tester):
       tester.dothat()
       assert 0 # for demo purpose

Liệu nó có thể đạt được nó như thế này hay thậm chí có một cách thanh lịch hơn?

Thông thường, tôi có thể làm điều đó cho mỗi phương pháp thử nghiệm với một số loại chức năng thiết lập (kiểu xUnit). Nhưng tôi muốn đạt được một số loại tái sử dụng. Có ai biết nếu điều này là có thể với đồ đạc ở tất cả?

Tôi biết tôi có thể làm điều gì đó như thế này: (từ tài liệu)

@pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"])

Nhưng tôi cần tham số hóa trực tiếp trong mô-đun thử nghiệm. Có thể truy cập thuộc tính params của vật cố định từ mô-đun thử nghiệm không?

Câu trả lời:


100

Cập nhật: Vì đây là câu trả lời được chấp nhận cho câu hỏi này và đôi khi vẫn được ủng hộ, tôi nên thêm một bản cập nhật. Mặc dù câu trả lời ban đầu của tôi (bên dưới) là cách duy nhất để thực hiện điều này trong các phiên bản pytest cũ hơn vì những người khác đã lưu ý rằng pytest hiện hỗ trợ tham số gián tiếp của đồ đạc. Ví dụ: bạn có thể làm điều gì đó như thế này (thông qua @imiric):

# test_parameterized_fixture.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [True, False], indirect=['tester'])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture.py::TestIt::test_tc1[True] PASSED                                                                                                                    [ 50%]
test_parameterized_fixture.py::TestIt::test_tc1[False] FAILED

Tuy nhiên, mặc dù hình thức tham số gián tiếp này là rõ ràng, như @Yukihiko Shinoda chỉ ra rằng nó hiện hỗ trợ hình thức tham số gián tiếp ngầm (mặc dù tôi không tìm thấy bất kỳ tham chiếu rõ ràng nào về điều này trong tài liệu chính thức):

# test_parameterized_fixture2.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [True, False])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture2.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture2.py::TestIt::test_tc1[True] PASSED                                                                                                                   [ 50%]
test_parameterized_fixture2.py::TestIt::test_tc1[False] FAILED

Tôi không biết chính xác ngữ nghĩa của biểu mẫu này là gì, nhưng có vẻ như nó pytest.mark.parametrizenhận ra rằng mặc dù test_tc1phương thức không có đối số được đặt tên tester_arg, nhưng testervật cố định mà nó đang sử dụng thì có, vì vậy nó truyền đối số được tham số hóa qua testervật thể.


Tôi đã gặp vấn đề tương tự - tôi có một bộ cố định được gọi test_packagevà sau đó tôi muốn có thể chuyển một đối số tùy chọn cho bộ cố định đó khi chạy nó trong các thử nghiệm cụ thể. Ví dụ:

@pytest.fixture()
def test_package(request, version='1.0'):
    ...
    request.addfinalizer(fin)
    ...
    return package

(Đối với những mục đích này, vật cố định làm gì hoặc loại đối tượng nào được trả về không quan trọng package).

Sau đó, tôi mong muốn bằng cách nào đó sử dụng vật cố định này trong một hàm thử nghiệm theo cách mà tôi cũng có thể chỉ định versionđối số cho vật cố định đó để sử dụng với thử nghiệm đó. Điều này hiện không thể thực hiện được, mặc dù có thể tạo ra một tính năng tốt.

Trong thời gian chờ đợi, thật dễ dàng để làm cho vật cố định của tôi chỉ cần trả lại một hàm thực hiện tất cả công việc mà vật cố định đã làm trước đó, nhưng cho phép tôi chỉ định versionđối số:

@pytest.fixture()
def test_package(request):
    def make_test_package(version='1.0'):
        ...
        request.addfinalizer(fin)
        ...
        return test_package

    return make_test_package

Bây giờ tôi có thể sử dụng điều này trong chức năng thử nghiệm của mình như:

def test_install_package(test_package):
    package = test_package(version='1.1')
    ...
    assert ...

và như thế.

Giải pháp cố gắng của OP đã đi đúng hướng và như câu trả lời của @ hpk42 cho thấy, giải pháp MyTester.__init__có thể chỉ lưu trữ tham chiếu đến yêu cầu như:

class MyTester(object):
    def __init__(self, request, arg=["var0", "var1"]):
        self.request = request
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

Sau đó, sử dụng điều này để triển khai các vật cố định như:

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester(request)
    return _tester

Nếu muốn, MyTesterlớp có thể được cấu trúc lại một chút để .argsthuộc tính của nó có thể được cập nhật sau khi nó được tạo, để điều chỉnh hành vi cho các bài kiểm tra riêng lẻ.


Cảm ơn về gợi ý với chức năng bên trong đồ đạc. Phải mất một thời gian cho đến khi tôi có thể làm việc này một lần nữa nhưng điều này khá hữu ích!
maggie

2
Một bài ngơi ngắn về chủ đề này: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie

bạn không gặp lỗi khi nói: "Các đồ đạc không được gọi trực tiếp, nhưng được tạo tự động khi các hàm kiểm tra yêu cầu chúng làm tham số."
nz_21

153

Điều này thực sự được hỗ trợ nguyên bản trong py.test thông qua tham số gián tiếp .

Trong trường hợp của bạn, bạn sẽ có:

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [['var1', 'var2']], indirect=True)
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

Ah, điều này khá hay (tôi nghĩ ví dụ của bạn có thể hơi lỗi thời - nó khác với các ví dụ trong tài liệu chính thức). Đây có phải là một tính năng tương đối mới? Tôi chưa bao giờ gặp nó trước đây. Đây cũng là một giải pháp tốt cho vấn đề - theo một số cách tốt hơn câu trả lời của tôi.
Iguananaut

2
Tôi đã thử sử dụng giải pháp này nhưng gặp sự cố khi truyền nhiều tham số hoặc sử dụng các tên biến khác với yêu cầu. Tôi đã kết thúc bằng cách sử dụng giải pháp của @Iguananaut.
Victor Uriarte

42
Đây phải là câu trả lời được chấp nhận. Các tài liệu chính thức cho indirectlập luận từ khóa là phải thừa nhận là thưa thớt và không thân thiện, mà có lẽ giải thích cho sự tối tăm của kỹ thuật thiết yếu này. Tôi đã tìm kiếm trang web py.test nhiều lần để tìm tính năng này - chỉ để tìm ra sản phẩm trống, cũ hơn và bị rối. Đắng lòng là nơi được gọi là tích hợp liên tục. Cảm ơn Odin về Stackoverflow.
Cecil Curry,

1
Lưu ý rằng phương pháp này thay đổi tên của các bài kiểm tra của bạn để bao gồm tham số, có thể mong muốn hoặc không. test_tc1trở thành test_tc1[tester0].
jjj

1
Vì vậy, indirect=Truebàn giao các tham số cho tất cả các đồ đạc được gọi là, phải không? Bởi vì tài liệu đặt tên rõ ràng cho các đồ đạc để tham số gián tiếp, ví dụ: cho đồ đạc có tên x:indirect=['x']
winklerrr

11

Bạn có thể truy cập mô-đun / lớp / chức năng yêu cầu từ các hàm fixture (và do đó từ lớp Tester của bạn), xem tương tác với việc yêu cầu ngữ cảnh kiểm tra từ một hàm fixture . Vì vậy, bạn có thể khai báo một số tham số trên một lớp hoặc mô-đun và người thử nghiệm có thể chọn nó.


3
Tôi biết mình có thể làm điều gì đó như sau: (từ docs) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Nhưng tôi cần phải làm điều đó trong mô-đun thử nghiệm. Làm cách nào tôi có thể thêm động các tham số vào đồ đạc?
maggie

2
Vấn đề là không phải tương tác với việc yêu cầu ngữ cảnh thử nghiệm từ một hàm fixture mà là phải có một cách được xác định rõ ràng để chuyển các đối số đến một hàm fixture. Hàm Fixture không cần phải biết về một loại ngữ cảnh kiểm tra yêu cầu chỉ để có thể nhận các đối số với các tên đã thống nhất. Ví dụ, một người muốn có thể viết @fixture def my_fixture(request)và sau đó @pass_args(arg1=..., arg2=...) def test(my_fixture)và nhận được các args my_fixture()như thế này arg1 = request.arg1, arg2 = request.arg2. Có điều gì như thế này có thể xảy ra trong py.test bây giờ không?
Piotr Dobrogost

7

Tôi không thể tìm thấy bất kỳ tài liệu nào, tuy nhiên, nó có vẻ hoạt động trong phiên bản mới nhất của pytest.

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [['var1', 'var2']])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

Cảm ơn bạn đã chỉ ra điều này - đây có vẻ như là giải pháp sạch sẽ nhất. Tôi không nghĩ rằng điều này từng có thể xảy ra trong các phiên bản trước đó, nhưng rõ ràng là bây giờ là như vậy. Bạn có biết nếu biểu mẫu này được đề cập ở bất kỳ đâu trong các tài liệu chính thức ? Tôi không thể tìm thấy bất cứ điều gì khá giống nó, nhưng nó rõ ràng hoạt động. Tôi đã cập nhật câu trả lời của mình để bao gồm ví dụ này, cảm ơn.
Iguananaut

1
Tôi nghĩ rằng nó sẽ không thể thực hiện được trong tính năng này, nếu bạn xem qua github.com/pytest-dev/pytest/issues/5712 và PR (hợp nhất) có liên quan.
Nadège

Điều này đã được hoàn nguyên github.com/pytest-dev/pytest/pull/6914
Maspe36

1
Để làm rõ, @ Maspe36 chỉ ra rằng PR được liên kết bởi Nadègeđã được hoàn nguyên. Do đó, tính năng không có giấy tờ này (tôi nghĩ rằng nó vẫn chưa có giấy tờ?) Vẫn tồn tại.
blthayer

6

Để cải thiện một chút câu trả lời của imiric : một cách thanh lịch khác để giải quyết vấn đề này là tạo "đồ đạc tham số". Cá nhân tôi thích nó hơn indirecttính năng của pytest. Tính năng này có sẵn từ pytest_cases, và ý tưởng ban đầu do Sup3rGeo đề xuất .

import pytest
from pytest_cases import param_fixture

# create a single parameter fixture
var = param_fixture("var", [['var1', 'var2']], ids=str)

@pytest.fixture
def tester(var):
    """Create tester object"""
    return MyTester(var)

class TestIt:
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

Lưu ý rằng điều đó pytest-casescũng cung cấp @pytest_fixture_pluscho phép bạn sử dụng các dấu tham số trên đồ đạc của mình và @cases_datacho phép bạn lấy nguồn tham số từ các hàm trong một mô-đun riêng biệt. Xem doc để biết chi tiết. Nhân tiện, tôi là tác giả;)


1
Điều này dường như cũng hoạt động trong pytest đơn giản (tôi có v5.3.1). Đó là, tôi đã có thể làm cho điều này hoạt động mà không cần param_fixture. Xem câu trả lời này . Tôi không thể tìm thấy bất kỳ ví dụ nào giống như nó trong tài liệu; Bạn biết gì về điều này không?
Iguananaut

Cảm ơn về thông tin và đường dẫn ! Tôi không biết điều này là khả thi. Chúng ta hãy chờ một tài liệu chính thức để xem những gì họ có trong đầu.
smarie

2

Tôi đã làm một bộ trang trí vui nhộn cho phép viết các đồ đạc như thế này:

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"

Ở đây, bên trái của /bạn có các đồ đạc khác và ở bên phải, bạn có các thông số được cung cấp bằng cách sử dụng:

@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"

Điều này hoạt động giống như cách hoạt động của các đối số hàm. Nếu bạn không cung cấp ageđối số, đối số mặc định 69, sẽ được sử dụng thay thế. nếu bạn không cung cấp namehoặc bỏ qua người dog.argumentstrang trí, bạn sẽ nhận được thông thường TypeError: dog() missing 1 required positional argument: 'name'. Nếu bạn có một thiết bị cố định khác có tranh luận name, nó không mâu thuẫn với thiết bị này.

Đồ đạc không đồng bộ cũng được hỗ trợ.

Ngoài ra, điều này cung cấp cho bạn một kế hoạch thiết lập đẹp:

$ pytest test_dogs_and_owners.py --setup-plan

SETUP    F dog['Buddy', age=7]
...
SETUP    F dog['Champion']
SETUP    F owner (fixtures used: dog)['John Travolta']

Một ví dụ đầy đủ:

from plugin import fixture_taking_arguments

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"


@fixture_taking_arguments
def owner(request, dog, /, name="John Doe"):
    yield f"{name}, owner of {dog}"


@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"


@dog.arguments("Champion")
class TestChampion:
    def test_with_dog(self, dog):
        assert dog == "Champion the dog aged 69"

    def test_with_default_owner(self, owner, dog):
        assert owner == "John Doe, owner of Champion the dog aged 69"
        assert dog == "Champion the dog aged 69"

    @owner.arguments("John Travolta")
    def test_with_named_owner(self, owner):
        assert owner == "John Travolta, owner of Champion the dog aged 69"

Mã cho người trang trí:

import pytest
from dataclasses import dataclass
from functools import wraps
from inspect import signature, Parameter, isgeneratorfunction, iscoroutinefunction, isasyncgenfunction
from itertools import zip_longest, chain


_NOTHING = object()


def _omittable_parentheses_decorator(decorator):
    @wraps(decorator)
    def wrapper(*args, **kwargs):
        if not kwargs and len(args) == 1 and callable(args[0]):
            return decorator()(args[0])
        else:
            return decorator(*args, **kwargs)
    return wrapper


@dataclass
class _ArgsKwargs:
    args: ...
    kwargs: ...

    def __repr__(self):
        return ", ".join(chain(
               (repr(v) for v in self.args), 
               (f"{k}={v!r}" for k, v in self.kwargs.items())))


def _flatten_arguments(sig, args, kwargs):
    assert len(sig.parameters) == len(args) + len(kwargs)
    for name, arg in zip_longest(sig.parameters, args, fillvalue=_NOTHING):
        yield arg if arg is not _NOTHING else kwargs[name]


def _get_actual_args_kwargs(sig, args, kwargs):
    request = kwargs["request"]
    try:
        request_args, request_kwargs = request.param.args, request.param.kwargs
    except AttributeError:
        request_args, request_kwargs = (), {}
    return tuple(_flatten_arguments(sig, args, kwargs)) + request_args, request_kwargs


@_omittable_parentheses_decorator
def fixture_taking_arguments(*pytest_fixture_args, **pytest_fixture_kwargs):
    def decorator(func):
        original_signature = signature(func)

        def new_parameters():
            for param in original_signature.parameters.values():
                if param.kind == Parameter.POSITIONAL_ONLY:
                    yield param.replace(kind=Parameter.POSITIONAL_OR_KEYWORD)

        new_signature = original_signature.replace(parameters=list(new_parameters()))

        if "request" not in new_signature.parameters:
            raise AttributeError("Target function must have positional-only argument `request`")

        is_async_generator = isasyncgenfunction(func)
        is_async = is_async_generator or iscoroutinefunction(func)
        is_generator = isgeneratorfunction(func)

        if is_async:
            @wraps(func)
            async def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_async_generator:
                    async for result in func(*args, **kwargs):
                        yield result
                else:
                    yield await func(*args, **kwargs)
        else:
            @wraps(func)
            def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_generator:
                    yield from func(*args, **kwargs)
                else:
                    yield func(*args, **kwargs)

        wrapper.__signature__ = new_signature
        fixture = pytest.fixture(*pytest_fixture_args, **pytest_fixture_kwargs)(wrapper)
        fixture_name = pytest_fixture_kwargs.get("name", fixture.__name__)

        def parametrizer(*args, **kwargs):
            return pytest.mark.parametrize(fixture_name, [_ArgsKwargs(args, kwargs)], indirect=True)

        fixture.arguments = parametrizer

        return fixture
    return decorator
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.