अध्याय 06

parametrize — एक टेस्ट, कई केस

कॉपी किए गए टेस्ट और टेस्ट के अंदर लूप की जगह @pytest.mark.parametrize। टेस्ट ID, ids= और pytest.param, decorator स्टैक करना, एक ही टेबल में वैल्यू और एरर, और आम गलतियाँ।

45 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

यह एक छोटा फ़ंक्शन है जो परीक्षा के अंकों को ग्रेड में बदलता है, grading.py:

python
def grade(score):
    if not 0 <= score <= 100:
        raise ValueError(f"score must be between 0 and 100, got {score}")
    if score >= 80:
        return "A"
    if score >= 60:
        return "B"
    if score >= 40:
        return "C"
    return "F"
text
$ python3 -c "from grading import grade; print(grade(95), grade(79), grade(40), grade(0))"
A B C F

इसे ठीक से टेस्ट करने का मतलब है कई अंकों को जाँचना, ख़ासकर किनारों वाले अंकों को: 80 और 79, 60 और 59, 40 और 39। इसे लिखने का पहला तरीक़ा जो हर किसी को सूझता है, वह है एक टेस्ट कॉपी करना और दो वैल्यूज़ बदल देना:

python
from grading import grade


def test_grade_95():
    assert grade(95) == "A"


def test_grade_79():
    assert grade(79) == "B"


def test_grade_40():
    assert grade(40) == "C"
text
$ pytest -q test_copy.py
...                                                                      [100%]
3 passed in 0.01s

यह काम करता है, पर बढ़ता नहीं। तीन फ़ंक्शन एक ही बात कहते हैं — "यह अंक यह ग्रेड देता है" — और उनमें सिर्फ़ दो वैल्यूज़ का फ़र्क़ है। दस और केस का मतलब है दस और कॉपियाँ, जिन्हें आपस में मेल में रखना होगा।

तो अगला विचार है: केसों की एक सूची, और एक ही टेस्ट के अंदर एक लूप। नीचे दो अपेक्षित ग्रेड जानबूझकर ग़लत हैं — 79 का B होना चाहिए, और 40 का C:

python
from grading import grade


def test_grades():
    cases = [(95, "A"), (80, "A"), (79, "C"), (40, "D"), (0, "F")]
    for score, expected in cases:
        assert grade(score) == expected
text
$ pytest -q --tb=short test_loop.py
F                                                                        [100%]
=================================== FAILURES ===================================
_________________________________ test_grades __________________________________
test_loop.py:7: in test_grades
    assert grade(score) == expected
E   AssertionError: assert 'B' == 'C'
E     
E     - C
E     + B
=========================== short test summary info ============================
FAILED test_loop.py::test_grades - AssertionError: assert 'B' == 'C'
1 failed in 0.01s

यह रिपोर्ट जितनी दिखती है उससे बुरी है। ग़लत केस दो थे, और ख़बर आपको एक की मिली: पहला फ़ेल होने वाला assert टेस्ट को वहीं ख़त्म कर देता है, इसलिए 40 वाला केस कभी चला ही नहीं। रिपोर्ट यह भी नहीं बताती कि कौन-सा अंक फ़ेल हुआ — आपको ख़ुद अंदाज़ा लगाना पड़ता है कि 'B' == 'C' 79 से आया। और "1 failed" पाँच केसों को एक ही तथ्य गिनता है। पहला ठीक करें, फिर चलाएँ, और तभी दूसरे से मुलाक़ात होगी।

आपको चाहिए लूप जितनी संक्षिप्तता और कॉपियों जैसी रिपोर्टिंग: एक फ़ंक्शन, कई अलग-अलग टेस्ट।

इस अध्याय के अंत में आप कर पाएंगे

  • @pytest.mark.parametrize से एक टेस्ट फ़ंक्शन को कई केसों पर चलाना
  • हर केस में कई वैल्यूज़ देना और pytest के बनाए टेस्ट ID पढ़ना
  • ids= और pytest.param(..., id=...) से केसों को नाम देना
  • बाक़ी केसों को छुए बिना किसी एक केस को xfail या skip मार्क करना
  • दो parametrize डेकोरेटर जोड़कर उनकी वैल्यूज़ की हर जोड़ी चलाना
  • "वैल्यू लौटाता है" और "एरर उठाता है" वाले केसों को एक ही टेबल में रखना
  • parametrize की आम ग़लतियों को उनके एरर मैसेज से पहचानना

ज़रूरी शर्तें: `pytest.approx` से float की तुलना।


टेस्ट लिखने से पहले

Parametrize असल में एक टेबल लिखने का तरीक़ा है। इसलिए किसी भी टेस्ट कोड से पहले का काम है वह टेबल बनाना, और यह तय करना कि टेबल सही रूप है भी या नहीं।

1. कॉन्ट्रैक्ट लिखें। grade(score) वादा करता है: 0 से 100 तक का एक पूर्णांक अंक अंदर जाता है; "A", "B", "C", "F" में से एक बाहर आता है; 0–100 के बाहर की कोई भी चीज़ ValueError उठाती है, जिसके मैसेज में वह अंक लिखा होता है। इसका कोई side effect नहीं है — न फ़ाइल, न प्रिंट — इसलिए न कुछ सेटअप करना है, न साफ़ करना। यही इसे उपयुक्त बनाता है: हर केस बस "ये इनपुट, वह नतीजा" है।

2. अपना सेटअप जाँचें। आपको सक्रिय virtual environment, इंस्टॉल किया हुआ pytest, और टेस्ट फ़ाइल से import हो सकने वाला grading.py चाहिए — इन उदाहरणों में वह टेस्टों के साथ उसी फ़ोल्डर में है। और कुछ नहीं: parametrize pytest में ही बना है, और contextlib स्टैंडर्ड लाइब्रेरी में है।

3. केसों की योजना बनाएँ। सबसे ज़्यादा मायने सीमाओं के हैं, क्योंकि >= बनाम > की ग़लतियाँ वहीं रहती हैं। हर नियम को उसकी रेखा के दोनों ओर की वैल्यू मिलती है:

| केस | इनपुट | अपेक्षित | |---|---|---| | रेंज का सबसे ऊपरी सिरा | 100 | "A" | | सबसे कम A | 80 | "A" | | सबसे ऊँचा B | 79 | "B" | | सबसे कम B | 60 | "B" | | सबसे ऊँचा C | 59 | "C" | | सबसे कम C | 40 | "C" | | सबसे ऊँचा F | 39 | "F" | | रेंज का सबसे निचला सिरा | 0 | "F" | | रेंज से ठीक नीचे | -1 | ValueError | | रेंज से ठीक ऊपर | 101 | ValueError |

हर पंक्ति एक केस बनती है। "केस" वाला कॉलम सजावट नहीं है — आगे चलकर वही टेस्ट ID बनता है, ताकि फ़ेल होने पर पता चले कि कौन-सा नियम टूटा।

4. तय करें कि क्या टेस्ट नहीं करना है। 0 से 100 तक हर अंक नहीं: 95 और 85 वही नियम जाँचते हैं जो 80, और बिना नई जानकारी जोड़े बस समय बढ़ाते हैं। हर सीमा के हर तरफ़ एक वैल्यू काफ़ी है। Python के अपने comparison operators को टेस्ट न करें, और दो अलग व्यवहारों को सिर्फ़ इसलिए एक टेस्ट में न डालें कि वे एक टेबल में समा जाते हैं — अगर पंक्तियों को अलग-अलग assertion चाहिए, तो वे अलग टेस्टों की हैं।

बाक़ी अध्याय इसी टेबल को एक-एक औज़ार से कोड में बदलता है।


एक आर्ग्युमेंट, कई वैल्यूज़

डेकोरेटर दो चीज़ें लेता है: एक आर्ग्युमेंट का नाम और उसके लिए वैल्यूज़ की सूची। Pytest हर वैल्यू के लिए एक टेस्ट बनाता है और उस वैल्यू को उसी नाम से अंदर भेजता है:

python
import pytest

from grading import grade


@pytest.mark.parametrize("score", [80, 95, 100])
def test_top_grade(score):
    assert grade(score) == "A"
text
$ pytest -v test_one.py
collecting ... collected 3 items

test_one.py::test_top_grade[80] PASSED                                   [ 33%]
test_one.py::test_top_grade[95] PASSED                                   [ 66%]
test_one.py::test_top_grade[100] PASSED                                  [100%]

============================== 3 passed in 0.01s ===============================

एक फ़ंक्शन, तीन टेस्ट, हर एक की अपनी लाइन और अपना नाम। वर्गाकार कोष्ठकों वाला हिस्सा टेस्ट ID है, जो वैल्यू से बना है। score कोई fixture नहीं है और उसकी कोई default वैल्यू नहीं — डेकोरेटर स्ट्रिंग का नाम बस फ़ंक्शन के किसी पैरामीटर से मेल खाना चाहिए।

हर केस में कई आर्ग्युमेंट

ज़्यादातर केसों को एक इनपुट और एक अपेक्षित नतीजा चाहिए। दोनों के नाम कॉमा से अलग करके लिखें, और हर केस को उसी क्रम में एक tuple के रूप में दें। यह रहा वही लूप फिर से, वही दो ग़लत अपेक्षाओं के साथ:

python
import pytest

from grading import grade


@pytest.mark.parametrize(
    "score,expected",
    [(95, "A"), (80, "A"), (79, "C"), (40, "D"), (0, "F")],
)
def test_grade(score, expected):
    assert grade(score) == expected
text
$ pytest -q --tb=short test_param.py
..FF.                                                                    [100%]
=================================== FAILURES ===================================
_______________________________ test_grade[79-C] _______________________________
test_param.py:11: in test_grade
    assert grade(score) == expected
E   AssertionError: assert 'B' == 'C'
E     
E     - C
E     + B
_______________________________ test_grade[40-D] _______________________________
test_param.py:11: in test_grade
    assert grade(score) == expected
E   AssertionError: assert 'C' == 'D'
E     
E     - D
E     + C
=========================== short test summary info ============================
FAILED test_param.py::test_grade[79-C] - AssertionError: assert 'B' == 'C'
FAILED test_param.py::test_grade[40-D] - AssertionError: assert 'C' == 'D'
2 failed, 3 passed in 0.01s

इसकी तुलना लूप से करें। हर केस चला। दोनों फ़ेल रिपोर्ट हुए, हर एक ऐसे ID के नीचे — test_grade[79-C] — जो केस का नाम बताता है। गिनती ईमानदार है: 2 failed, 3 passed। (डिफ़ॉल्ट लंबे traceback में pytest हर फ़ेल केस के आर्ग्युमेंट भी छापता है, score = 79, expected = 'C'।)

नाम एक स्ट्रिंग के रूप में लिखे जा सकते हैं, "score,expected" या "score, expected" — कॉमा के बाद स्पेस चलता है — या स्ट्रिंग्स की सूची के रूप में, ["score", "expected"]। दोनों बिलकुल एक जैसे टेस्ट बनाते हैं। स्ट्रिंग आम रूप है; सूची तब काम आती है जब नाम कोड से बनाए जा रहे हों।

ID कैसे बनते हैं

संख्याओं, स्ट्रिंग्स, booleans और None के लिए pytest वैल्यू को ही ID में रख देता है और एक केस की वैल्यूज़ को - से जोड़ता है। बाक़ी किसी भी चीज़ के लिए — list, dict, object — वह कोशिश ही नहीं करता। वह आर्ग्युमेंट का नाम और केस की स्थिति इस्तेमाल करता है:

python
import pytest


@pytest.mark.parametrize(
    ["value", "expected"],
    [
        (1.5, True),
        ("hello world", None),
        ([1, 2], {"a": 1}),
    ],
)
def test_show_ids(value, expected):
    pass
text
$ pytest -v test_ids_auto.py
collecting ... collected 3 items

test_ids_auto.py::test_show_ids[1.5-True] PASSED                         [ 33%]
test_ids_auto.py::test_show_ids[hello world-None] PASSED                 [ 66%]
test_ids_auto.py::test_show_ids[value2-expected2] PASSED                 [100%]

============================== 3 passed in 0.01s ===============================

value2-expected2 का मतलब है "केस नंबर 2 का value और expected, शून्य से गिनते हुए"। यह अनोखा है, और कुछ नहीं बताता। ऐसा केस फ़ेल हो तो उसे ढूँढने के लिए आपको सूची में नीचे गिनना पड़ता है। यही वजह है कि केसों को नाम ख़ुद दें।

केसों को नाम देना: ids=

ids= को स्ट्रिंग्स की एक सूची दें, हर केस के लिए एक, उसी क्रम में:

python
import pytest

from grading import grade


@pytest.mark.parametrize(
    "score,expected",
    [(80, "A"), (79, "B"), (40, "C"), (39, "F")],
    ids=["lowest-A", "highest-B", "lowest-C", "highest-F"],
)
def test_grade_boundaries(score, expected):
    assert grade(score) == expected
text
$ pytest -v test_ids_list.py
collecting ... collected 4 items

test_ids_list.py::test_grade_boundaries[lowest-A] PASSED                 [ 25%]
test_ids_list.py::test_grade_boundaries[highest-B] PASSED                [ 50%]
test_ids_list.py::test_grade_boundaries[lowest-C] PASSED                 [ 75%]
test_ids_list.py::test_grade_boundaries[highest-F] PASSED                [100%]

============================== 4 passed in 0.01s ===============================

अब आउटपुट बताता है कि हर केस क्यों है। कमज़ोरी यह है कि नाम एक दूसरी सूची में रहते हैं, अपने केसों से दूर: बीच में एक केस जोड़ें, उसका नाम भूल जाएँ, और उसके बाद का हर नाम एक जगह खिसक जाएगा।

ids= एक फ़ंक्शन भी लेता है। Pytest उसे हर वैल्यू के लिए एक बार बुलाता है, हर केस के लिए नहीं, और नतीजों को - से जोड़ता है:

python
import pytest

from grading import grade


def describe(value):
    if isinstance(value, int):
        return f"score={value}"
    return f"grade={value}"


@pytest.mark.parametrize("score,expected", [(80, "A"), (79, "B")], ids=describe)
def test_grade_named(score, expected):
    assert grade(score) == expected
text
$ pytest -v test_ids_func.py
collecting ... collected 2 items

test_ids_func.py::test_grade_named[score=80-grade=A] PASSED              [ 50%]
test_ids_func.py::test_grade_named[score=79-grade=B] PASSED              [100%]

============================== 2 passed in 0.01s ===============================

फ़ंक्शन उन वैल्यूज़ के लिए ठीक है जिनका अपने-आप बना ID बेकार है, जैसे objects। अगर वह किसी वैल्यू के लिए None लौटाए, तो pytest उस वैल्यू के लिए अपने-आप बने ID पर लौट आता है।

एक बार में एक केस: pytest.param

pytest.param एक अकेले केस को लपेटता है और उसमें चीज़ें जोड़ने देता है: एक id= जो उसकी वैल्यूज़ के ठीक बगल में रहता है, और marks= जो सिर्फ़ उसी केस पर लागू होते हैं।

python
import pytest

from grading import grade


@pytest.mark.parametrize(
    "score,expected",
    [
        pytest.param(100, "A", id="perfect"),
        pytest.param(60, "B", id="lowest-B"),
        pytest.param(59.5, "B", id="round-up", marks=pytest.mark.xfail(reason="no rounding yet")),
        pytest.param(0, "F", id="zero", marks=pytest.mark.skip(reason="example of skip")),
    ],
)
def test_grade_cases(score, expected):
    assert grade(score) == expected
text
$ pytest -v test_param_obj.py
collecting ... collected 4 items

test_param_obj.py::test_grade_cases[perfect] PASSED                      [ 25%]
test_param_obj.py::test_grade_cases[lowest-B] PASSED                     [ 50%]
test_param_obj.py::test_grade_cases[round-up] XFAIL (no rounding yet)    [ 75%]
test_param_obj.py::test_grade_cases[zero] SKIPPED (example of skip)      [100%]

=================== 2 passed, 1 skipped, 1 xfailed in 0.01s ====================

round-up केस एक ऐसा फ़ैसला दर्ज करता है जो अभी लिया नहीं गया — क्या 59.5 ऊपर होकर B बनना चाहिए? — और वह भी रन को फ़ेल किए बिना; बाक़ी केस अछूते रहते हैं। (xfail और skip का आगे अपना अध्याय है; अभी इतना जानना काफ़ी है कि एक मार्क एक केस पर लग सकता है।)

चूँकि ID एक असली नाम है, एक केस को अकेले चलाया जा सकता है: pytest "test_param_obj.py::test_grade_cases[perfect]"। इसे quotes में रखें — शेल वर्गाकार कोष्ठकों को ख़ास तरीक़े से समझता है।

दो डेकोरेटर: हर संयोजन

दो parametrize डेकोरेटर एक के ऊपर एक रखें और pytest उनकी वैल्यूज़ की हर जोड़ी के लिए टेस्ट चलाता है — cartesian product। दो मुद्राएँ गुणा तीन रक़में, यानी छह टेस्ट:

python
import pytest


@pytest.mark.parametrize("currency", ["USD", "EUR"])
@pytest.mark.parametrize("amount", [0, 10, 999])
def test_format(amount, currency):
    text = f"{amount} {currency}"
    assert text.endswith(currency)
text
$ pytest -v test_stack.py
collecting ... collected 6 items

test_stack.py::test_format[0-USD] PASSED                                 [ 16%]
test_stack.py::test_format[0-EUR] PASSED                                 [ 33%]
test_stack.py::test_format[10-USD] PASSED                                [ 50%]
test_stack.py::test_format[10-EUR] PASSED                                [ 66%]
test_stack.py::test_format[999-USD] PASSED                               [ 83%]
test_stack.py::test_format[999-EUR] PASSED                               [100%]

============================== 6 passed in 0.01s ===============================

फ़ंक्शन के सबसे पास वाला डेकोरेटर ID का पहला हिस्सा देता है। स्टैक तभी करें जब इनपुट सच में स्वतंत्र हों — हर रक़म हर मुद्रा के साथ चलनी चाहिए। अगर सिर्फ़ कुछ जोड़ियाँ ही मायने रखती हैं, तो उन्हें एक डेकोरेटर में tuples के रूप में लिखें। और गुणा पर नज़र रखें: दस-दस वैल्यूज़ वाले तीन डेकोरेटर हज़ार टेस्ट बनाते हैं।

वैल्यूज़ और एरर एक ही टेबल में

कभी-कभी केस दो हिस्सों में बँट जाते हैं: "यह लौटाता है" और "वह उठाता है"। आप अपेक्षा को ही एक पैरामीटर बना सकते हैं, और with से उसमें प्रवेश कर सकते हैं:

python
from contextlib import nullcontext

import pytest

from grading import grade


@pytest.mark.parametrize(
    "score,expectation",
    [
        (50, nullcontext("C")),
        (100, nullcontext("A")),
        (-1, pytest.raises(ValueError)),
        (101, pytest.raises(ValueError, match="got 101")),
    ],
)
def test_grade_or_error(score, expectation):
    with expectation as expected:
        assert grade(score) == expected
text
$ pytest -v test_raises.py
collecting ... collected 4 items

test_raises.py::test_grade_or_error[50-expectation0] PASSED              [ 25%]
test_raises.py::test_grade_or_error[100-expectation1] PASSED             [ 50%]
test_raises.py::test_grade_or_error[-1-expectation2] PASSED              [ 75%]
test_raises.py::test_grade_or_error[101-expectation3] PASSED             [100%]

============================== 4 passed in 0.01s ===============================

contextlib.nullcontext एक context manager है जो कुछ नहीं करता: with nullcontext("C") as expected बस expected = "C" सेट करता है, इसलिए सामान्य केस assert को हमेशा की तरह चलाते हैं। एरर वाले केसों में pytest.raises context manager है: grade उसके अंदर एरर उठाता है, assert कभी पूरा नहीं होता, और टेस्ट पास होता है क्योंकि सही एरर आया। अगर कोई एरर न आए, तो आपको जाना-पहचाना DID NOT RAISE मिलता है।

छोटी टेबल के लिए यह साफ़-सुथरा है। अगर टेस्ट के अंदर दोनों तरह के केसों को अलग पहचानने के लिए if लिखने पड़ें, तो उसे दो टेस्टों में बाँट दें। ID पर भी ध्यान दें: expectation0 फिर से वही object वाला नियम है। पूरा उदाहरण इसे pytest.param से ठीक करता है।

एक क्लास को parametrize करना

डेकोरेटर को एक टेस्ट क्लास पर लगाएँ और उसके अंदर का हर टेस्ट मेथड वह पैरामीटर पाता है:

python
import pytest

from grading import grade


@pytest.mark.parametrize("score", [80, 100])
class TestTopBand:
    def test_is_a(self, score):
        assert grade(score) == "A"

    def test_is_a_string(self, score):
        assert isinstance(grade(score), str)
text
$ pytest -v test_class.py
collecting ... collected 4 items

test_class.py::TestTopBand::test_is_a[80] PASSED                         [ 25%]
test_class.py::TestTopBand::test_is_a[100] PASSED                        [ 50%]
test_class.py::TestTopBand::test_is_a_string[80] PASSED                  [ 75%]
test_class.py::TestTopBand::test_is_a_string[100] PASSED                 [100%]

============================== 4 passed in 0.01s ===============================

दो मेथड गुणा दो वैल्यूज़: चार टेस्ट। क्लास का हर मेथड score स्वीकार करे, वरना collection फ़ेल होगा।


पूर्ण उदाहरण

shipping.py:

python
import math

RATES = {"local": 50, "national": 120}


def shipping_cost(weight_kg, zone):
    if zone not in RATES:
        raise KeyError(f"unknown zone: {zone}")
    if weight_kg <= 0:
        raise ValueError("weight must be positive")
    # The first kilogram is in the base rate; each started kilogram after it costs 20.
    return RATES[zone] + (math.ceil(weight_kg) - 1) * 20
text
$ python3 -c "from shipping import shipping_cost; print(shipping_cost(0.5, 'local'), shipping_cost(1.2, 'local'), shipping_cost(3, 'national'))"
50 70 160

test_shipping.py:

python
from contextlib import nullcontext

import pytest

from shipping import shipping_cost


@pytest.mark.parametrize(
    "weight_kg,zone,expected",
    [
        pytest.param(0.5, "local", 50, id="light-local"),
        pytest.param(1, "local", 50, id="exactly-1kg"),
        pytest.param(1.2, "local", 70, id="part-kg-rounds-up"),
        pytest.param(3, "national", 160, id="heavy-national"),
    ],
)
def test_cost(weight_kg, zone, expected):
    assert shipping_cost(weight_kg, zone) == expected


@pytest.mark.parametrize("zone", ["local", "national"])
@pytest.mark.parametrize("weight_kg", [0.1, 0.5, 1])
def test_first_kg_is_flat(weight_kg, zone):
    assert shipping_cost(weight_kg, zone) == shipping_cost(1, zone)


@pytest.mark.parametrize(
    "weight_kg,zone,expectation",
    [
        pytest.param(2, "local", nullcontext(70), id="ok"),
        pytest.param(0, "local", pytest.raises(ValueError), id="zero-weight"),
        pytest.param(-1, "local", pytest.raises(ValueError), id="negative"),
        pytest.param(1, "moon", pytest.raises(KeyError, match="moon"), id="bad-zone"),
    ],
)
def test_cost_or_error(weight_kg, zone, expectation):
    with expectation as expected:
        assert shipping_cost(weight_kg, zone) == expected
text
$ pytest -v test_shipping.py
collecting ... collected 14 items

test_shipping.py::test_cost[light-local] PASSED                          [  7%]
test_shipping.py::test_cost[exactly-1kg] PASSED                          [ 14%]
test_shipping.py::test_cost[part-kg-rounds-up] PASSED                    [ 21%]
test_shipping.py::test_cost[heavy-national] PASSED                       [ 28%]
test_shipping.py::test_first_kg_is_flat[0.1-local] PASSED                [ 35%]
test_shipping.py::test_first_kg_is_flat[0.1-national] PASSED             [ 42%]
test_shipping.py::test_first_kg_is_flat[0.5-local] PASSED                [ 50%]
test_shipping.py::test_first_kg_is_flat[0.5-national] PASSED             [ 57%]
test_shipping.py::test_first_kg_is_flat[1-local] PASSED                  [ 64%]
test_shipping.py::test_first_kg_is_flat[1-national] PASSED               [ 71%]
test_shipping.py::test_cost_or_error[ok] PASSED                          [ 78%]
test_shipping.py::test_cost_or_error[zero-weight] PASSED                 [ 85%]
test_shipping.py::test_cost_or_error[negative] PASSED                    [ 92%]
test_shipping.py::test_cost_or_error[bad-zone] PASSED                    [100%]

============================== 14 passed in 0.01s ==============================

तीन फ़ंक्शन, चौदह टेस्ट, और हर लाइन बताती है कि उसने क्या जाँचा। तीन बातें ध्यान देने लायक़ हैं।

पहली, test_cost के ID हर केस की वजह बताते हैं — part-kg-rounds-up उस नियम का नाम है जिसकी रक्षा हो रही है, जो 1.2-local-70 नहीं बताता।

दूसरी, test_first_kg_is_flat स्टैक करता है क्योंकि वज़न और ज़ोन सच में स्वतंत्र हैं। वह फ़ंक्शन की तुलना किसी संख्या से नहीं बल्कि ख़ुद से करता है, इसलिए दरें बदलने पर भी टिका रहता है।

तीसरी, pytest.param(..., id=...) expectation2 को negative बना देता है। वैल्यूज़ और एरर की टेबल अब एक specification की तरह पढ़ी जाती है।


जब यह काम न करे

function uses no argument 'scores' डेकोरेटर स्ट्रिंग का कोई नाम फ़ंक्शन के किसी पैरामीटर से मेल नहीं खाता — यहाँ "scores,expected" बनाम def test_grade(score, expected)। यह collection के समय फ़ेल होता है, कोई टेस्ट चलने से पहले:

python
import pytest

from grading import grade


@pytest.mark.parametrize("scores,expected", [(80, "A"), (40, "C")])
def test_grade(score, expected):
    assert grade(score) == expected
text
$ pytest -q test_mismatch.py

==================================== ERRORS ====================================
______________________ ERROR collecting test_mismatch.py _______________________
In test_mismatch.py::test_grade: function uses no argument 'scores'
=========================== short test summary info ============================
ERROR test_mismatch.py - Failed: In test_mismatch.py::test_grade: function us...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01s

fixture 'expected' not found उलटी ग़लती: फ़ंक्शन में एक ऐसा पैरामीटर है जिसका नाम डेकोरेटर में कभी नहीं आता, इसलिए pytest उस नाम का fixture ढूँढने जाता है और कुछ नहीं पाता। नाम को डेकोरेटर स्ट्रिंग में जोड़ें।

the number of names (2) ... must be equal to the number of values (1) किसी एक केस में वैल्यूज़ की संख्या ग़लत है — मान लीजिए "score,expected" वाली सूची में (40,):

text
$ pytest -q test_count.py

==================================== ERRORS ====================================
________________________ ERROR collecting test_count.py ________________________
test_count.py::test_grade: in "parametrize" the number of names (2):
  ['score', 'expected']
must be equal to the number of values (1):
  (40,)

[score0] जैसे ID और tuple का ज़िक्र करता TypeError एक आर्ग्युमेंट नाम के साथ, सूची की हर वैल्यू पूरी की पूरी भेजी जाती है। आदतन वहाँ tuples लिख दें तो फ़ंक्शन को tuple मिलता है:

python
import pytest

from grading import grade


@pytest.mark.parametrize("score", [(80,), (95,)])
def test_top_grade(score):
    assert grade(score) == "A"
text
$ pytest -v --tb=no test_tuples.py
collecting ... collected 2 items

test_tuples.py::test_top_grade[score0] FAILED                            [ 50%]
test_tuples.py::test_top_grade[score1] FAILED                            [100%]

=========================== short test summary info ============================
FAILED test_tuples.py::test_top_grade[score0] - TypeError: '<=' not supported...
FAILED test_tuples.py::test_top_grade[score1] - TypeError: '<=' not supported...
============================== 2 failed in 0.01s ===============================

ID ही सुराग़ है: सादी संख्या होती तो [80] मिलता, इसलिए [score0] बताता है कि वैल्यू कुछ और थी। एक आर्ग्युमेंट के लिए [80, 95] लिखें।

एक केस अकेले पास होता है और बाक़ियों के साथ फ़ेल पैरामीटर वैल्यूज़ एक बार बनती हैं, जब मॉड्यूल collect होता है, और वही object हर उस टेस्ट को दिया जाता है जो उसे पाता है। अगर कोई टेस्ट एक mutable वैल्यू — list, dict — को बदल दे, तो अगला टेस्ट वह बदलाव देखता है:

python
import pytest


@pytest.mark.parametrize("cart", [[]])
class TestCart:
    def test_add_item(self, cart):
        cart.append("pen")
        assert cart == ["pen"]

    def test_starts_empty(self, cart):
        assert cart == []
text
$ pytest -q --tb=short test_shared.py
.F                                                                       [100%]
=================================== FAILURES ===================================
______________________ TestCart.test_starts_empty[cart0] _______________________
test_shared.py:11: in test_starts_empty
    assert cart == []
E   AssertionError: assert ['pen'] == []
E     
E     Left contains one more item: 'pen'
E     Use -v to get more diff
=========================== short test summary info ============================
FAILED test_shared.py::TestCart::test_starts_empty[cart0] - AssertionError: a...
1 failed, 1 passed in 0.01s

कोई immutable चीज़ दें, जैसे tuple (), और हर टेस्ट के अंदर cart = list(items) से नया object बनाएँ। तब हर टेस्ट अपनी ख़ाली सूची से शुरू होता है और दोनों पास होते हैं।