অধ্যায় 07

Fixture — টেস্টের প্রস্তুতি এক জায়গায়

একই setup বারবার না লিখে @pytest.fixture দিয়ে একবার লিখুন, নাম ধরে চেয়ে নিন, প্রতিটি টেস্টে নতুন কপি পান, আর yield দিয়ে টেস্ট fail করলেও পরিষ্কার করুন।

45 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

এই হলো একটি ছোট শপিং কার্ট, cart.py:

python
class Cart:
    def __init__(self):
        self.lines = []

    def add(self, name, price, quantity=1):
        if quantity < 1:
            raise ValueError(f"quantity must be at least 1: {quantity}")
        self.lines.append((name, price, quantity))

    def total(self):
        return sum(price * quantity for _, price, quantity in self.lines)

    def count(self):
        return sum(quantity for _, _, quantity in self.lines)

আর এর জন্য তিনটি test, test_cart.py:

python
from cart import Cart


def test_total():
    cart = Cart()
    cart.add("pen", 15.0, 3)
    cart.add("bag", 850.0)
    assert cart.total() == 895.0


def test_count():
    cart = Cart()
    cart.add("pen", 15.0, 3)
    cart.add("bag", 850.0)
    assert cart.count() == 4


def test_add_one_more():
    cart = Cart()
    cart.add("pen", 15.0, 3)
    cart.add("bag", 850.0)
    cart.add("notebook", 60.0, 2)
    assert cart.total() == 1015.0
text
pytest -q
...                                                                      [100%]
3 passed in 0.01s

এগুলো pass করে, কিন্তু পড়তে ক্লান্তিকর। তিনটি test, নয় লাইন setup, আর মাত্র তিনটি লাইন যা আসলে কিছু যাচাই করে। setup প্রতিটিতে একই, তাই যে একটি লাইন আলাদা — যে লাইনের জন্যই test-টি লেখা — সেটিই চাপা পড়ে যায়।

পরেও এর একটা দাম দিতে হয়। যেদিন Cart()-এর একটি argument লাগবে, সেদিন আপনাকে তিন জায়গায় বদলাতে হবে, কিংবা ত্রিশ জায়গায়। গত অধ্যায়ে parametrize একটি test-এর input-এর পুনরাবৃত্তি দূর করেছিল। এই অধ্যায় দূর করবে তার setup-এর পুনরাবৃত্তি।

এই অধ্যায় শেষে আপনি পারবেন

  • @pytest.fixture দিয়ে একটি fixture লিখতে, আর test-এর parameter হিসেবে তার নাম লিখে সেটি চাইতে
  • বোঝাতে কেন প্রতিটি test নিজের একটি নতুন কপি পায়, আর একটি তালিকা দিয়ে তা দেখাতে
  • একটি fixture দিয়ে আরেকটি fixture বানাতে
  • এমন একটি yield fixture লিখতে যা নিজের পরে নিজেই পরিষ্কার করে, আর setup ও teardown-এর ক্রম আগে থেকে বলতে
  • --setup-show দিয়ে fixture তৈরি হওয়া চোখে দেখতে
  • fixture 'x' not found আর "called directly" ত্রুটি পড়ে বুঝতে
  • fixture আর একটি সাধারণ helper function-এর মধ্যে বেছে নিতে

আগে যা জানা লাগবে: `parametrize` দিয়ে একটি test অনেকগুলো ক্ষেত্রে চালানো।


টেস্ট লেখার আগে

fixture এমন কিছু নয় যা test লেখার পরে জুড়ে দেওয়া হয়। এগুলো একটি পরিকল্পনা থেকে আপনাআপনি বেরিয়ে আসে, আর পরিকল্পনাটি আসে সবার আগে।

কী প্রতিশ্রুতি দেওয়া হয়েছে। Cart তিনটি প্রতিশ্রুতি দেয়: add() একটি লাইন লিখে রাখে, আর 1-এর কম quantity হলে ValueError দিয়ে প্রত্যাখ্যান করে; total() হলো দাম × quantity-র যোগফল; count() হলো quantity-গুলোর যোগফল। কোনো ফাইল নেই, network নেই, global অবস্থা নেই — অর্থাৎ এখন পর্যন্ত ফিরিয়ে নেওয়ার মতো কিছু নেই।

কী আগে থেকে প্রস্তুত থাকতে হবে। প্রতিটি অধ্যায়ের মতোই: pytest ইনস্টল করা একটি virtual environment, আর যে ফোল্ডার থেকে আপনি pytest চালান সেখান থেকে cart.py import করা যায় এমন অবস্থা (test ফাইলটি তার পাশেই রাখুন)।

কোন কোন ক্ষেত্র। সেগুলো লিখে ফেলুন, আর একটি কলাম যোগ করুন যা মানুষ সাধারণত বাদ দেয় — test যে অবস্থা থেকে শুরু হয়:

| ক্ষেত্র | শুরুর অবস্থা | কাজ | প্রত্যাশিত ফল | |---|---|---|---| | সাধারণ মোট | pen × 3, bag × 1 | total() | 895.0 | | জিনিসের সংখ্যা | pen × 3, bag × 1 | count() | 4 | | আরও একটি লাইন | pen × 3, bag × 1 | add("notebook", 60.0, 2) | total() হবে 1015.0 | | প্রান্তিক ক্ষেত্র: কিছুই নেই | খালি কার্ট | total() | 0 | | অবৈধ quantity | খালি কার্ট | add("pen", 15.0, 0) | ValueError |

দ্বিতীয় কলামটি উপর থেকে নিচে পড়ুন। তিনটি সারি শুরু হয় একই ভরা কার্ট থেকে, দুটি একটি খালি কার্ট থেকে। ওই কলামের প্রতিটি পুনরাবৃত্ত মান হলো একটি fixture, যা লেখার অপেক্ষায় আছে — এখানে empty_cart, আর তার উপর বানানো একটি cart। আর যে সারি এমন একটি অবস্থা থেকে শুরু হয় যা আর কেউ ভাগ করে না, তার কোনো fixture-ই দরকার নেই; সে নিজের অবস্থা নিজেই বানায়।

প্রতিটি শুরুর অবস্থাকে আরও একটি প্রশ্ন করুন: এটি তৈরি করলে কি test-এর বাইরে কিছু বদলায়? একটি নতুন Cart বদলায় না। একটি global dictionary-তে register করা discount code, একটি খোলা connection, একটি লেখা ফাইল — এগুলো বদলায়, আর ঠিক এটাই আপনাকে বলে দেয় যে একটি fixture-এর পরিষ্কারের কাজ দরকার (yield, এই অধ্যায়ে পরে আসছে)।

কী test করবেন না। pytest প্রতিটি test-এর জন্য নতুন fixture বানায় — এটি প্রমাণ করতে test লিখবেন না; সেটি pytest-এর প্রতিশ্রুতি, আর এই অধ্যায় একবার তা দেখিয়ে দেয় যাতে আপনি ভরসা করতে পারেন। কিন্তু আপনার নিজের পরিষ্কারের কাজ যখন global অবস্থা ফিরিয়ে আনে, সেটি অবশ্যই test করুন — ওই code আপনার, আর সেটি ভুল হতে পারে।

অধ্যায়ের বাকি অংশ এই পরিকল্পনাকে code-এ রূপ দেয়।

প্রথম fixture

setup-টিকে একটি function-এ সরিয়ে নিন, আর তার উপরে @pytest.fixture বসান:

python
import pytest

from cart import Cart


@pytest.fixture
def cart():
    c = Cart()
    c.add("pen", 15.0, 3)
    c.add("bag", 850.0)
    return c


def test_total(cart):
    assert cart.total() == 895.0


def test_count(cart):
    assert cart.count() == 4


def test_add_one_more(cart):
    cart.add("notebook", 60.0, 2)
    assert cart.total() == 1015.0
text
pytest -q
...                                                                      [100%]
3 passed in 0.01s

কোনো test-ই cart() call করে না। প্রতিটি test-এর কেবল cart নামে একটি parameter আছে, আর চাওয়া বলতে ওইটুকুই।

একটি test চালানোর আগে pytest তার parameter-গুলোর নাম দেখে। প্রতিটি নামের জন্য সে ওই নামের একটি fixture খুঁজে বের করে, সেটি চালায়, আর যা ফেরত এল তা argument হিসেবে test-এ পাঠিয়ে দেয়। test বলে তার কী দরকার; সেটি নিজে বানায় না। এই ব্যবস্থার একটি নাম আছে — dependency injection — কিন্তু কৌশলটি «parameter-এর নাম fixture-এর নামের সাথে মেলানো»-র বেশি কিছু নয়।

test-টি এখন উদ্দেশ্যের একটিমাত্র লাইন হিসেবে পড়া যায়। আর setup থাকে এক জায়গায়, তাই Cart()-এ কোনো পরিবর্তন মানে একটিমাত্র function-এ পরিবর্তন।

প্রতিটি test পায় একটি নতুন কপি

test_add_one_more কার্টে একটি notebook যোগ করে। এর পরে যে test-গুলো চলল, তারা কি notebook-সহ একটি কার্ট দেখল? না — আর এটি পরিষ্কারভাবে দেখে নেওয়া দরকার, কারণ এই বৈশিষ্ট্যটিই fixture-কে নিরাপদ করে।

আগে fixture ছাড়া সংস্করণটি দেখি। module-এর উপরে একটি তালিকা, যা দুটি test ভাগ করে ব্যবহার করে:

python
ITEMS = ["pen", "bag"]


def test_add_item():
    ITEMS.append("notebook")
    assert len(ITEMS) == 3


def test_starts_with_two():
    assert len(ITEMS) == 2
text
pytest -q
.F                                                                       [100%]
=================================== FAILURES ===================================
_____________________________ test_starts_with_two _____________________________

    def test_starts_with_two():
>       assert len(ITEMS) == 2
E       AssertionError: assert 3 == 2
E        +  where 3 = len(['pen', 'bag', 'notebook'])

test_shared.py:10: AssertionError
=========================== short test summary info ============================
FAILED test_shared.py::test_starts_with_two - AssertionError: assert 3 == 2
1 failed, 1 passed in 0.01s

দ্বিতীয় test-টি ঠিকই আছে; সেটি fail করেছে প্রথম test-টি যা করেছে তার কারণে। একা চালালে এটি pass করে:

text
pytest -q test_shared.py::test_starts_with_two
.                                                                        [100%]
1 passed in 0.01s

যে test-এর ফল নির্ভর করে তার আগে কোন test-গুলো চলেছে তার উপর, সেটি একটি test suite-এর সবচেয়ে ব্যয়বহুল bug-গুলোর একটি, কারণ এটি দেখা দেয় কেবল মাঝে মাঝে।

এবার একই দুটি test, তালিকাটি আসছে একটি fixture থেকে। fixture-এর ভেতরে একটি print দেখাবে সেটি কখন চলে; -s pytest-কে বলে ছাপা output আটকে না রাখতে, যাতে আমরা তা দেখতে পাই:

python
import pytest


@pytest.fixture
def items():
    print("building a new list")
    return ["pen", "bag"]


def test_add_item(items):
    items.append("notebook")
    assert len(items) == 3


def test_starts_with_two(items):
    assert len(items) == 2
text
pytest -q -s
building a new list
.building a new list
.
2 passed in 0.01s

"building a new list" দুবার এসেছে। fixture function-টি চলেছে যে test তাকে চেয়েছে তার প্রত্যেকটির জন্য একবার করে, আর প্রতিটি test এমন একটি তালিকা পেয়েছে যা আর কেউ ছোঁয়নি। এটিই default, আর এর একটি নাম আছে, function scope: fixture-এর মান টিকে থাকে একটি test function যতক্ষণ চলে ততক্ষণ। অষ্টম অধ্যায়ে দেখানো হবে এটি কীভাবে বদলানো যায়, আর কেন সাধারণত বদলানো উচিত নয়।

fixture-ও fixture ব্যবহার করতে পারে

একটি fixture আরেকটি fixture চাইতে পারে ঠিক একইভাবে, যেভাবে একটি test চায় — parameter হিসেবে তার নাম লিখে:

python
import pytest

from cart import Cart


@pytest.fixture
def empty_cart():
    return Cart()


@pytest.fixture
def cart(empty_cart):
    empty_cart.add("pen", 15.0, 3)
    empty_cart.add("bag", 850.0)
    return empty_cart


def test_new_cart_is_empty(empty_cart):
    assert empty_cart.total() == 0


def test_total(cart):
    assert cart.total() == 895.0
text
pytest -q
..                                                                       [100%]
2 passed in 0.01s

cart তৈরি হয় empty_cart-এর উপর, তাই একটি test প্রস্তুতির যে স্তরটি দরকার সেটিই চাইতে পারে। কোনো test কিছুই call করে না; pytest নামগুলো অনুসরণ করে, test_total থেকে cart-এ আর cart থেকে empty_cart-এ, আর সেই শৃঙ্খল যে ক্রম দাবি করে সেই ক্রমেই এগুলো বানায়।

চোখে দেখা — --setup-show

কোন fixture-গুলো চলল তা জানতে আপনাকে print যোগ করতে হবে না। --setup-show প্রতিটি setup আর teardown ছেপে দেয়:

text
pytest -q --setup-show

        SETUP    F empty_cart
        test_cart.py::test_new_cart_is_empty (fixtures used: empty_cart) .
        TEARDOWN F empty_cart
        SETUP    F empty_cart
        SETUP    F cart (fixtures used: empty_cart)
        test_cart.py::test_total (fixtures used: cart, empty_cart) .
        TEARDOWN F cart
        TEARDOWN F empty_cart
2 passed in 0.01s

উপর থেকে পড়ুন। F মানে function scope — একটি test-এর জন্য বানানো, আর তার পরে ফেলে দেওয়া। empty_cart দুবার setup হয়েছে, প্রতিটি test-এর জন্য একবার — আগের অংশের সেই নতুনত্ব। যেখানে cart ব্যবহৃত হয়েছে, সেখানে empty_cart আগে setup হয়েছে, কারণ cart তার উপর নির্ভর করে। আর teardown-গুলো আসে setup-এর উল্টো ক্রমে: সবার শেষে যেটি বানানো, সেটিই সবার আগে সরানো।

(আপনার plugin ইনস্টল করা থাকলে তাদের নিজস্ব fixture-এর জন্য বাড়তি লাইন দেখা যেতে পারে। সেগুলো তাদের, আপনার নয়।)

এখন পর্যন্ত teardown চোখে পড়ার মতো কিছু করেনি, কারণ ফিরিয়ে নেওয়ার মতো কিছু ছিল না। পরের অংশে সেটি বদলাবে।

yield — আগে setup, তারপর পরিষ্কার

কিছু setup পেছনে এমন কিছু রেখে যায় যা ফিরিয়ে নিতে হয়: একটি connection বন্ধ করতে হবে, একটি ফাইল মুছতে হবে, একটি global setting আগের অবস্থায় ফেরাতে হবে। এর জন্য fixture return-এর বদলে yield ব্যবহার করে:

python
import pytest


@pytest.fixture
def connection():
    print("\nconnection: open")
    conn = {"orders": []}
    yield conn
    print("connection: close")


def test_starts_empty(connection):
    print("test: running")
    assert connection["orders"] == []
text
pytest -q -s

connection: open
test: running
.connection: close

1 passed in 0.01s

yield-এর আগের সবকিছু setup। yield-এর পরের মানটিই test পায়। তারপর fixture থেমে থাকে — test চলে — আর test শেষ হলে pytest fixture-টিকে yield-এর পর থেকে আবার চালু করে, আর সেই বাকি অংশটিই teardown। ("connection: close"-এর সামনে . বসে আছে কারণ test শেষ হওয়ার মুহূর্তেই তার ফল ছাপা হয়, teardown শুরু হওয়ার ঠিক আগে।)

পরিষ্কারের কাজটি থাকে setup-এর সাথে একই function-এ, তার কয়েক লাইন নিচে। একটি যোগ করে অন্যটি ভুলে যাবেন, আর তা চোখে পড়বে না — এমন হওয়ার উপায় নেই।

দুটি yield fixture থাকলে, একটি অন্যটির উপর নির্ভরশীল, ক্রমটি সেটাই যার ইঙ্গিত --setup-show দিয়েছিল:

python
import pytest


@pytest.fixture
def connection():
    print("\n1. connection: open")
    conn = {"orders": []}
    yield conn
    print("5. connection: close")


@pytest.fixture
def saved_order(connection):
    print("2. saved_order: insert")
    connection["orders"].append("A-100")
    yield "A-100"
    connection["orders"].remove("A-100")
    print("4. saved_order: delete")


def test_order_is_saved(connection, saved_order):
    print("3. test: running")
    assert saved_order in connection["orders"]
text
pytest -q -s

1. connection: open
2. saved_order: insert
3. test: running
.4. saved_order: delete
5. connection: close

1 passed in 0.01s

setup চলে নির্ভরতার শৃঙ্খলের নিচ থেকে উপরে; teardown ফেরে আবার নিচের দিকে। কাজ করে কেবল এই ক্রমটিই: saved_order তার row মোছে connection-এর মাধ্যমে, তাই মোছার সময় connection-টি তখনও খোলা থাকতে হবে।

test fail করলেও teardown চলে

test নিজেই শেষে পরিষ্কার করে নিতে পারলে, পরিষ্কারের জন্য fixture-এর ঝামেলায় যাওয়া কেন? চেষ্টা করে দেখুন। OPEN এখানে এমন যেকোনো কিছুর প্রতিনিধি যা একটি test পেছনে রেখে যেতে পারে — একটি খোলা connection, একটি অস্থায়ী ফাইল, একটি বদলানো setting:

python
OPEN = []


def test_broken():
    OPEN.append("connection")
    assert OPEN == ["A-100"]
    OPEN.remove("connection")


def test_nothing_left_open():
    assert OPEN == []
text
pytest -q --tb=no
FF                                                                       [100%]
=========================== short test summary info ============================
FAILED test_cleanup_in_body.py::test_broken - AssertionError: assert ['connec...
FAILED test_cleanup_in_body.py::test_nothing_left_open - AssertionError: asse...
2 failed in 0.01s

(--tb=no traceback লুকিয়ে রাখে, কেবল সারসংক্ষেপটুকু রাখে।) একটি ভুল, দুটি fail। একটি fail করা assert exception raise করে, আর raise করা মানে test সেখানেই শেষ, তাই তার নিচের পরিষ্কারের লাইনটি কখনো চলেনি। connection-টি OPEN-এ থেকে গেছে, আর পরের test — যা পুরোপুরি সঠিক ছিল — উত্তরাধিকারসূত্রে পাওয়া জিনিসের কারণে fail করেছে। একটি বড় suite-এ এই দ্বিতীয় fail-টির পেছনেই আপনার একটা গোটা বিকেল চলে যেত।

এবার একই কাজ, পরিষ্কারের কাজটি একটি yield-এর পরে:

python
import pytest

OPEN = []


@pytest.fixture
def connection():
    OPEN.append("connection")
    yield "connection"
    OPEN.remove("connection")


def test_broken(connection):
    assert OPEN == ["A-100"]


def test_nothing_left_open():
    assert OPEN == []
text
pytest -q --tb=no
F.                                                                       [100%]
=========================== short test summary info ============================
FAILED test_cleanup_in_fixture.py::test_broken - AssertionError: assert ['con...
1 failed, 1 passed in 0.01s

একটি ভুল, একটি fail। ভাঙা test-টির খবর তবু আসে — fixture কিছুই লুকায় না — কিন্তু তার ছড়ানো জঞ্জাল পরের test পর্যন্ত পৌঁছায়নি।

এটাই কারণ, আর এর থেকে আসা নিয়মটি হলো: পরিষ্কারের কাজ কখনো test-এর শরীরের শেষে যায় না। সেটি যায় একটি fixture-এর yield-এর পরে।

fixture, নাকি সাধারণ function?

setup ভাগ করার একমাত্র উপায় fixture নয়। একটি সাধারণ function — ধরুন cart_with(*lines), যা তাকে দেওয়া লাইনগুলো দিয়ে একটি কার্ট বানায় — সেটিও কাজ করে, আর নিচের সম্পূর্ণ উদাহরণে একটি এমন function ব্যবহার করা হয়েছে।

সিদ্ধান্ত নেয় যে পার্থক্যটি, সেটি এই: একটি test একটি helper-কে call করে, আর তাকে argument দিতে পারে। একটি test fixture-কে call করতে পারে না; কেবল তার নাম লিখতে পারে। একটি fixture যে test-ই চাক, সবার জন্য সবসময় একই জিনিস তৈরি করে।

তাই:

  • fixture ব্যবহার করুন যখন অনেক test-এর একই শুরুর অবস্থা দরকার, বিশেষ করে যখন সেই অবস্থা পরে পরিষ্কার করতে হয়। নিশ্চিত teardown কেবল fixture-ই পায়।
  • helper function ব্যবহার করুন যখন প্রতিটি test-এর একটু আলাদা একটি অবজেক্ট দরকার — এই লাইনগুলোসহ একটি কার্ট, ওই নামের একজন user। argument তো function-এর জন্যই।

দুটি ভালোভাবে একসাথে চলে: একটি fixture একটি helper-কে call করতে পারে, যেমনটি পরের উদাহরণে করা হয়েছে। (একটি fixture বানানোর এমন উপায়ও আছে যা একটি function ফেরত দেয় — দ্বাদশ অধ্যায়ের "factory" pattern। এখনই সেটি দরকার নেই।)


একটা সম্পূর্ণ উদাহরণ

একটি দোকান, যেখানে discount code থাকে module-স্তরের একটি dictionary-তে। একটি code register করলে global অবস্থা বদলায়, তাই যে test একটি code register করে, তাকে সেটি আবার সরিয়ে ফেলতে হবে।

shop.py:

python
DISCOUNTS = {}


def register_discount(code, percent):
    DISCOUNTS[code] = percent


def remove_discount(code):
    del DISCOUNTS[code]


class Cart:
    def __init__(self):
        self.lines = []

    def add(self, name, price, quantity=1):
        if quantity < 1:
            raise ValueError(f"quantity must be at least 1: {quantity}")
        self.lines.append((name, price, quantity))

    def total(self, code=None):
        amount = sum(price * quantity for _, price, quantity in self.lines)
        if code is not None:
            amount = amount * (100 - DISCOUNTS[code]) / 100
        return round(amount, 2)

test_shop.py:

python
import pytest

from shop import Cart, register_discount, remove_discount


def cart_with(*lines):
    # A plain helper: it takes arguments, so each test can ask for its own cart.
    cart = Cart()
    for name, price, quantity in lines:
        cart.add(name, price, quantity)
    return cart


@pytest.fixture
def cart():
    # The cart most tests share. Built fresh for every test that asks.
    return cart_with(("pen", 15.0, 3), ("bag", 850.0, 1))


@pytest.fixture
def summer_code():
    # Changes global state, so it must undo that change afterwards.
    register_discount("SUMMER", 10)
    yield "SUMMER"
    remove_discount("SUMMER")


def test_total(cart):
    assert cart.total() == 895.0


def test_summer_discount(cart, summer_code):
    assert cart.total(summer_code) == 805.5


def test_code_is_gone_afterwards(cart):
    with pytest.raises(KeyError):
        cart.total("SUMMER")


def test_small_cart(summer_code):
    cart = cart_with(("pen", 15.0, 2))
    assert cart.total(summer_code) == 27.0


def test_zero_quantity_is_refused():
    with pytest.raises(ValueError):
        Cart().add("pen", 15.0, 0)
text
pytest -q --setup-show

        SETUP    F cart
        test_shop.py::test_total (fixtures used: cart) .
        TEARDOWN F cart
        SETUP    F cart
        SETUP    F summer_code
        test_shop.py::test_summer_discount (fixtures used: cart, summer_code) .
        TEARDOWN F summer_code
        TEARDOWN F cart
        SETUP    F cart
        test_shop.py::test_code_is_gone_afterwards (fixtures used: cart) .
        TEARDOWN F cart
        SETUP    F summer_code
        test_shop.py::test_small_cart (fixtures used: summer_code) .
        TEARDOWN F summer_code
        test_shop.py::test_zero_quantity_is_refused .
5 passed in 0.01s

তিনটি জিনিস লক্ষ করুন।

প্রথমত, প্রতিটি test কেবল সেটুকুই চায় যা সে ব্যবহার করে। test_small_cart-এর একটি আলাদা কার্ট দরকার ছিল, তাই সে cart চায়ইনি; সে নিজের লাইনগুলো দিয়ে helper-টিকে call করেছে — আর --setup-show নিশ্চিত করছে যে তার জন্য cart কখনো বানানোই হয়নি। test_zero_quantity_is_refused-এর দরকার কেবল একটি খালি কার্ট আর একটি call, তাই সে কোনো fixture ব্যবহার করে না; পরিকল্পনার সেই «এমন শুরুর অবস্থা যা আর কেউ ভাগ করে না» সারিটি এটাই।

দ্বিতীয়ত, test_code_is_gone_afterwards আসলে fixture-এর একটি test। এটি চলে test_summer_discount-এর পরে, আর যাচাই করে যে SUMMER আর নেই। কেন এটি গুরুত্বপূর্ণ তা দেখতে, summer_code-এ yield আর পরিষ্কারের লাইনটির বদলে return "SUMMER" লিখে দিন:

text
pytest -q --tb=no
..F..                                                                    [100%]
=========================== short test summary info ============================
FAILED test_shop.py::test_code_is_gone_afterwards - Failed: DID NOT RAISE Key...
1 failed, 4 passed in 0.01s

code-টি যে test তাকে register করেছিল, সেখান থেকে চুইয়ে পরের test-এ চলে গেছে। ওই যাচাইটি না থাকলে এই চুইয়ে পড়া নীরবই থেকে যেত।

তৃতীয়ত, helper আর fixture একসাথে কাজ করে। cart_with জানে কীভাবে একটি কার্ট বানাতে হয়; cart fixture ঠিক করে বেশিরভাগ test কোন কার্টটি ভাগ করবে।


কিছু ভাঙা অবস্থা ও তার সমাধান

fixture 'crat' not found

text
_________________________ ERROR at setup of test_empty _________________________
file .../test_typo.py, line 11
  def test_empty(crat):
E       fixture 'crat' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, cart, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

একটি parameter-এর নাম কোনো fixture-এর সাথে মেলেনি। হয় বানান ভুল — এখানে cart-এর বদলে crat, আর cart তো তালিকাতেই আছে — নয়তো function-টির উপরে @pytest.fixture লাইনটি নেই, সেক্ষেত্রে তালিকায় সেটি থাকবে না। খেয়াল করুন, এখানে E আর ERROR, F আর FAILED নয়: test-টি শুরুই হয়নি।

Fixture "cart" called directly

text
Fixture "cart" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.

test-টি তার শরীরে cart() লিখেছে। একটি fixture চাওয়া হয় parameter হিসেবে তার নাম লিখে, কখনো call করে নয়। যদি সত্যিই নিজের argument দিয়ে সেটি call করতে চান, তাহলে আপনার আসলে দরকার একটি helper function।

fail-এর বদলে ERROR at setup of test_total exception-টি উঠেছে fixture-এর ভেতরে, test চলার আগেই — যেমন setup-এ একটি ভুল add() থেকে ValueError: quantity must be at least 1: 0। E মানে «প্রস্তুতি ভেঙেছে»; F মানে «দাবিটি মিথ্যা ছিল»। আগে fixture-এ খুঁজুন।

fixture function has more than one 'yield' একটি fixture ঠিক একবার yield করে। setup থাকে yield-এর উপরে, পরিষ্কারের কাজ নিচে; দুটি মানের জন্য একটি tuple yield করুন, অথবা দুটি fixture লিখুন।

একটি test একা pass করে, অন্যদের সাথে fail করে test-গুলোর মধ্যে কিছু একটা ভাগ হচ্ছে — module-স্তরের একটি তালিকা বা dictionary, কিংবা এমন একটি global যা কোনো fixture বদলেছে কিন্তু আগের অবস্থায় ফেরায়নি। সেটি একটি fixture-এ বানান, আর যদি সেটি global অবস্থা হয়, তাহলে yield-এর পরে পরিবর্তনটি ফিরিয়ে নিন।

পরিষ্কারের কাজটি চলেনি পরিষ্কারের কাজটি test-এর শরীরে, fail করা একটি assert-এর পরে লেখা, অথবা fixture-টি return ব্যবহার করছে। এটি একটি yield fixture-এ সরিয়ে নিন, yield-এর পরে।