ส่งผ่านพารามิเตอร์ไปยังฟังก์ชันฟิกซ์เจอร์


114

ฉันใช้ py.test เพื่อทดสอบโค้ด DLL บางตัวที่ห่อด้วย Python class MyTester เพื่อวัตถุประสงค์ในการตรวจสอบความถูกต้องฉันจำเป็นต้องบันทึกข้อมูลการทดสอบบางส่วนในระหว่างการทดสอบและดำเนินการเพิ่มเติมในภายหลัง เนื่องจากฉันมีไฟล์ทดสอบ _... จำนวนมากฉันจึงต้องการใช้การสร้างวัตถุทดสอบซ้ำ (อินสแตนซ์ของ MyTester) สำหรับการทดสอบส่วนใหญ่ของฉัน

เนื่องจากวัตถุทดสอบเป็นวัตถุที่ได้รับการอ้างอิงถึงตัวแปรและฟังก์ชันของ DLL ฉันจึงต้องส่งรายการตัวแปรของ DLL ไปยังวัตถุผู้ทดสอบสำหรับไฟล์ทดสอบแต่ละไฟล์ (ตัวแปรที่จะบันทึกจะเหมือนกันสำหรับ test_ .. . ไฟล์). เนื้อหาของรายการจะถูกใช้เพื่อบันทึกข้อมูลที่ระบุ

ความคิดของฉันคือทำแบบนี้:

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

เป็นไปได้หรือไม่ที่จะประสบความสำเร็จเช่นนี้หรือมีวิธีที่หรูหรากว่านี้?

โดยปกติฉันสามารถทำได้สำหรับแต่ละวิธีการทดสอบด้วยฟังก์ชันการตั้งค่าบางประเภท แต่ฉันต้องการนำกลับมาใช้ใหม่ ไม่มีใครรู้ว่าสิ่งนี้เป็นไปได้ด้วยการติดตั้งหรือไม่?

ฉันรู้ว่าฉันสามารถทำสิ่งนี้ได้: (จากเอกสาร)

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

แต่ฉันต้องการพาราเมตริเซชั่นโดยตรงในโมดูลทดสอบ สามารถเข้าถึงแอตทริบิวต์ params ของฟิกซ์เจอร์จากโมดูลทดสอบได้หรือไม่?

คำตอบ:


101

อัปเดต:เนื่องจากนี่เป็นคำตอบที่ยอมรับสำหรับคำถามนี้และยังคงได้รับการโหวตในบางครั้งฉันจึงควรเพิ่มการอัปเดต ถึงแม้ว่าคำตอบเดิมของฉัน (ด้านล่าง) เป็นวิธีเดียวที่จะทำเช่นนี้ในรุ่นเก่าของ pytest เป็นคนอื่น ๆได้ตั้งข้อสังเกต pytest ขณะนี้สนับสนุนและตัวแปรทางอ้อมของการแข่งขัน ตัวอย่างเช่นคุณสามารถทำสิ่งนี้ได้ (ผ่านทาง @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

อย่างไรก็ตามแม้ว่ารูปแบบของพารามิเตอร์ทางอ้อมแบบนี้จะมีความชัดเจนเนื่องจาก @Yukihiko Shinoda ชี้ให้เห็นว่าตอนนี้สนับสนุนรูปแบบของพารามิเตอร์ทางอ้อมโดยปริยาย (แม้ว่าฉันจะไม่พบการอ้างอิงที่ชัดเจนเกี่ยวกับสิ่งนี้ในเอกสารอย่างเป็นทางการ):

# 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

ผมไม่ทราบว่าสิ่งที่มีความหมายของรูปแบบนี้ แต่มันก็ดูเหมือนว่าpytest.mark.parametrizeตระหนักดีว่าถึงแม้ว่าtest_tc1วิธีการไม่ใช้อาร์กิวเมนต์ชื่อtester_argที่testerติดตั้งว่ามันใช้ไม่ดังนั้นมันผ่านอาร์กิวเมนต์ parametrized บนผ่านtesterการติดตั้ง


ฉันมีปัญหาที่คล้ายกัน - ฉันมีฟิกซ์เจอร์ที่เรียกว่าtest_packageและในภายหลังฉันต้องการที่จะสามารถส่งอาร์กิวเมนต์ที่เป็นทางเลือกไปยังฟิกซ์เจอร์นั้นเมื่อเรียกใช้ในการทดสอบเฉพาะ ตัวอย่างเช่น:

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

(ไม่สำคัญสำหรับวัตถุประสงค์เหล่านี้ว่าฟิกซ์เจอร์ทำอะไรหรือประเภทของวัตถุที่ส่งคืนpackage) คือ

จากนั้นจึงเป็นที่พึงปรารถนาที่จะใช้ฟิกซ์เจอร์นี้ในฟังก์ชันทดสอบในลักษณะที่ฉันสามารถระบุversionอาร์กิวเมนต์ของฟิกซ์เจอร์นั้นเพื่อใช้กับการทดสอบนั้นได้ ขณะนี้ยังไม่สามารถทำได้แม้ว่าอาจเป็นคุณลักษณะที่ดี

ในระหว่างนี้มันง่ายพอที่จะทำให้ฟิกซ์เจอร์ของฉันส่งคืนฟังก์ชันที่ทำงานทั้งหมดที่ฟิกซ์เจอร์ก่อนหน้านี้ทำ แต่ช่วยให้ฉันระบุversionอาร์กิวเมนต์:

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

    return make_test_package

ตอนนี้ฉันสามารถใช้สิ่งนี้ในฟังก์ชันทดสอบของฉันเช่น:

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

และอื่น ๆ

โซลูชันที่พยายามของ OP มุ่งไปในทิศทางที่ถูกต้องและเป็นของ @ hpk42 คำตอบแนะนำMyTester.__init__ก็สามารถเก็บข้อมูลอ้างอิงไปยังคำขอเช่น:

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"

จากนั้นใช้สิ่งนี้เพื่อติดตั้งฟิกซ์เจอร์เช่น:

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

หากต้องการMyTesterชั้นเรียนสามารถปรับโครงสร้างเล็กน้อยเพื่อให้.argsสามารถอัปเดตแอตทริบิวต์ได้หลังจากสร้างเสร็จแล้วเพื่อปรับเปลี่ยนพฤติกรรมสำหรับการทดสอบแต่ละครั้ง


ขอบคุณสำหรับคำแนะนำเกี่ยวกับฟังก์ชั่นภายในฟิกซ์เจอร์ ใช้เวลาสักพักจนกว่าฉันจะสามารถทำสิ่งนี้ได้อีกครั้ง แต่ก็มีประโยชน์มาก!
maggie

2
โพสต์สั้น ๆ ที่ดีในหัวข้อนี้: alysivji.github.io/pytest-fixures-with-function-arguments.html
maggie

คุณไม่ได้รับข้อผิดพลาดที่แจ้งว่า: "การแข่งขันไม่ได้ถูกเรียกใช้โดยตรง แต่จะสร้างขึ้นโดยอัตโนมัติเมื่อฟังก์ชันทดสอบขอให้เป็นพารามิเตอร์"?
nz_21

154

สิ่งนี้ได้รับการสนับสนุนโดยกำเนิดใน py.test ผ่านทางพารามิเตอร์ทางอ้อมและตัวแปรทางอ้อม

ในกรณีของคุณคุณจะมี:

@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

นี่เป็นสิ่งที่ดีทีเดียว (ฉันคิดว่าตัวอย่างของคุณอาจจะล้าสมัยไปหน่อย แต่มันแตกต่างจากตัวอย่างในเอกสารทางการ) นี่เป็นคุณสมบัติที่ค่อนข้างใหม่หรือไม่? ฉันไม่เคยเจอมาก่อน นี่เป็นวิธีแก้ปัญหาที่ดีเช่นกัน - ในบางวิธีก็ดีกว่าคำตอบของฉัน
Iguananaut

2
ฉันลองใช้โซลูชันนี้ แต่มีปัญหาในการส่งผ่านพารามิเตอร์หลายตัวหรือใช้ชื่อตัวแปรนอกเหนือจากคำขอ ฉันลงเอยด้วยการใช้โซลูชันของ @Iguananaut
Victor Uriarte

42
นี่ควรเป็นคำตอบที่ได้รับการยอมรับ เอกสารอย่างเป็นทางการสำหรับindirectการโต้แย้งคำหลักที่เป็นที่ยอมรับเบาบางและไม่เป็นมิตรซึ่งอาจบัญชีสำหรับความสับสนของเทคนิคที่สำคัญนี้ ฉันได้กวาดล้างเว็บไซต์ py.test หลายครั้งสำหรับคุณลักษณะนี้ - เพียงเพื่อให้ว่างเปล่าเก่ากว่าและสับสน ความขมขื่นเป็นสถานที่ที่เรียกว่าการผสมผสานอย่างต่อเนื่อง ขอบคุณ Odin สำหรับ Stackoverflow
Cecil Curry

1
หมายเหตุวิธีนี้จะเปลี่ยนชื่อการทดสอบของคุณเพื่อรวมพารามิเตอร์ซึ่งอาจต้องการหรือไม่ก็ได้ จะกลายเป็นtest_tc1 test_tc1[tester0]
jjj

1
ดังนั้นindirect=Trueมอบพารามิเตอร์ให้กับอุปกรณ์ที่เรียกว่าทั้งหมดใช่ไหม? เนื่องจากเอกสารประกอบระบุชื่อการแข่งขันสำหรับพารามีทรีเซชันทางอ้อมอย่างชัดเจนเช่นสำหรับฟิกซ์เจอร์ที่ชื่อx:indirect=['x']
winklerrr

11

คุณสามารถเข้าถึงขอโมดูล / ระดับ / ฟังก์ชั่นจากฟังก์ชั่นการติดตั้ง (และจากชั้น Tester ของคุณ) ดูมีปฏิสัมพันธ์กับบริบทขอทดสอบจากฟังก์ชั่นอุปกรณ์ติดตั้ง ดังนั้นคุณสามารถประกาศพารามิเตอร์บางอย่างในคลาสหรือโมดูลและฟิกซ์เจอร์ผู้ทดสอบสามารถเลือกได้


3
ฉันรู้ว่าฉันสามารถทำสิ่งนี้ได้: (จากเอกสาร) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) แต่ฉันต้องทำใน โมดูลทดสอบ ฉันจะเพิ่มพารามิเตอร์แบบไดนามิกลงในส่วนควบได้อย่างไร
maggie

2
ประเด็นคือไม่จำเป็นต้องโต้ตอบกับการร้องขอบริบทการทดสอบจากฟังก์ชันฟิกซ์เจอร์ แต่ต้องมีวิธีที่กำหนดไว้อย่างดีในการส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชันฟิกซ์เจอร์ ฟังก์ชัน Fixture ไม่ควรต้องตระหนักถึงประเภทของบริบทการทดสอบที่ร้องขอเพื่อให้สามารถรับข้อโต้แย้งที่มีชื่อตกลงกันได้ ตัวอย่างหนึ่งต้องการที่จะสามารถที่จะเขียน@fixture def my_fixture(request)แล้ว@pass_args(arg1=..., arg2=...) def test(my_fixture)และได้รับ args เหล่านี้ในการเช่นนี้my_fixture() arg1 = request.arg1, arg2 = request.arg2สิ่งนี้เป็นไปได้ใน py.test หรือไม่?
Piotr Dobrogost

7

ฉันไม่พบเอกสารใด ๆ เลยดูเหมือนว่าจะใช้งานได้กับ 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

ขอขอบคุณที่ชี้ให้เห็นสิ่งนี้ดูเหมือนจะเป็นวิธีแก้ปัญหาที่สะอาดที่สุด ฉันไม่คิดว่าสิ่งนี้จะเคยเป็นไปได้ในเวอร์ชันก่อนหน้านี้ แต่ตอนนี้ก็ชัดเจนแล้ว คุณทราบหรือไม่ว่ามีการกล่าวถึงแบบฟอร์มนี้ในเอกสารทางการหรือไม่ ฉันไม่พบสิ่งที่ถูกใจ แต่มันใช้งานได้อย่างชัดเจน ฉันได้อัปเดตคำตอบเพื่อรวมตัวอย่างนี้แล้วขอบคุณ
Iguananaut

1
ฉันคิดว่าจะเป็นไปไม่ได้ในคุณลักษณะนี้หากคุณดูที่github.com/pytest-dev/pytest/issues/5712และ PR ที่เกี่ยวข้อง (รวม)
Nadège

สิ่งนี้ถูกเปลี่ยนกลับgithub.com/pytest-dev/pytest/pull/6914
Maspe36

1
เพื่อชี้แจง @ Maspe36 ระบุว่า PR ที่เชื่อมโยงโดยNadègeถูกเปลี่ยนกลับ ดังนั้นคุณสมบัติที่ไม่มีเอกสารนี้ (ฉันคิดว่ามันยังไม่มีเอกสาร?) ยังคงมีอยู่
blthayer

6

เพื่อปรับปรุงคำตอบของ imiricเล็กน้อย: อีกวิธีหนึ่งที่ยอดเยี่ยมในการแก้ปัญหานี้คือการสร้าง "การติดตั้งพารามิเตอร์" โดยส่วนตัวแล้วฉันชอบมันมากกว่าindirectคุณสมบัติของpytest. คุณลักษณะนี้สามารถใช้ได้จากpytest_casesและความคิดเดิมได้รับการแนะนำโดยSup3rGeo

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

โปรดทราบว่าpytest-casesยังให้@pytest_fixture_plusที่อนุญาตให้คุณใช้เครื่องหมายพาราเมตริเซชั่นบนส่วนควบของคุณและ@cases_dataอนุญาตให้คุณกำหนดแหล่งพารามิเตอร์ของคุณจากฟังก์ชันในโมดูลแยกต่างหาก ดูรายละเอียดในเอกสาร ฉันเป็นคนเขียนเอง;)


1
ดูเหมือนว่าจะใช้งานได้ใน pytest ธรรมดาเช่นกัน (ฉันมี v5.3.1) param_fixtureนั่นคือผมสามารถที่จะได้รับการทำงานนี้ได้โดยไม่ต้อง ดูคำตอบนี้ ฉันไม่พบตัวอย่างเช่นในเอกสารแม้ว่า; คุณรู้อะไรเกี่ยวกับเรื่องนี้ไหม
Iguananaut

ขอบคุณสำหรับข้อมูลและลิงค์! ฉันไม่รู้ว่ามันเป็นไปได้ รอเอกสารอย่างเป็นทางการเพื่อดูว่าพวกเขาคิดอะไรอยู่
smarie

2

ฉันสร้างมัณฑนากรตลกที่อนุญาตให้เขียนส่วนควบเช่นนี้:

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

ที่นี่ทางด้านซ้ายของ/คุณมีส่วนควบอื่น ๆ และทางด้านขวาคุณมีพารามิเตอร์ที่ให้มาโดยใช้:

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

วิธีนี้ใช้ได้ผลเช่นเดียวกับอาร์กิวเมนต์ของฟังก์ชัน หากคุณไม่ระบุageอาร์กิวเมนต์ค่าเริ่มต้น69จะถูกใช้แทน ถ้าคุณไม่ได้จัดหาnameหรือละเว้นdog.argumentsมัณฑนากร, TypeError: dog() missing 1 required positional argument: 'name'คุณจะได้รับตามปกติ หากคุณมีอุปกรณ์อื่นที่สามารถโต้แย้งnameได้ก็จะไม่ขัดแย้งกับสิ่งนี้

นอกจากนี้ยังรองรับการติดตั้ง Async

นอกจากนี้ยังช่วยให้คุณมีแผนการติดตั้งที่ดี:

$ 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']

ตัวอย่างเต็ม:

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"

รหัสสำหรับมัณฑนากร:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.