ข้ามไปยังเนื้อหา

pytest — เชิงลึก

Playground note: pytest ต้องการ binary pytest และ test discovery context — รัน locally ด้วย pytest code blocks ด้านล่างได้รับการตรวจสอบด้วย python3 ในส่วนที่ทำได้; การรัน test เต็มรูปแบบต้องใช้ environment ใน local

แนวคิด Jestคู่เทียบ pytest
describe blockการจัดกลุ่มด้วย module หรือ class (optional)
it / testฟังก์ชันที่ชื่อขึ้นต้นด้วย test_*
beforeEach / afterEachfixture ที่มี yield
beforeAll / afterAllfixture ที่มี scope session/module
it.each / test.each@pytest.mark.parametrize
expect(x).toBe(y)assert x == y (พร้อม introspection)
jest.fn() / jest.mock()fixture monkeypatch / unittest.mock

fixture คือฟังก์ชันที่ครอบด้วย @pytest.fixture แล้ว pytest จะ inject เข้าไปให้ทุก test ที่ประกาศชื่อไว้เป็น parameter บทบาทเทียบได้กับ beforeEach ของ Jest แต่ทำได้มากกว่า เพราะ fixture compose กันได้ คือ fixture ตัวหนึ่ง depend บน fixture ตัวอื่นต่อไปอีกทอดได้

TypeScript
// Jest — beforeEach for setup
describe("UserService", () => {
let db: Database;
let service: UserService;
beforeEach(async () => {
db = await Database.connect(":memory:");
service = new UserService(db);
});
afterEach(async () => {
await db.close();
});
it("creates a user", async () => {
const user = await service.create({ name: "Alice" });
expect(user.id).toBeDefined();
});
});
Python
# pytest — fixture with yield (setup + teardown)
import pytest
@pytest.fixture
def db():
connection = {"connected": True, "data": {}}
yield connection # test ได้รับค่านี้
connection["connected"] = False # teardown หลัง yield
@pytest.fixture
def user_service(db): # fixture ที่ depend บน fixture อื่น
return {"db": db, "users": db["data"]}
def test_create_user(user_service):
user_service["users"]["alice"] = {"name": "Alice"}
assert "alice" in user_service["users"]
def test_db_connected(db):
assert db["connected"] is True

รัน locally:

Terminal window
pip install pytest
pytest test_users.py -v

Fixtures ค่าเริ่มต้นคือ scope="function" (สร้างใหม่ต่อ test) scopes ที่กว้างกว่าหลีกเลี่ยง setup ที่ cost สูง:

@pytest.fixture(scope="module") # ครั้งเดียวต่อ test module
def db_connection():
conn = create_db_connection()
yield conn
conn.close()
@pytest.fixture(scope="session") # ครั้งเดียวตลอดการรัน pytest
def auth_token():
return get_test_token()
Scopeสร้างเมื่อไหร่
function (ค่าเริ่มต้น)ก่อนทุก test
classครั้งเดียวต่อ test class
moduleครั้งเดียวต่อ .py file
sessionครั้งเดียวต่อการเรียก pytest

@pytest.mark.parametrize คือ it.each ของ Jest — รัน test เดียวกันด้วย input/output หลายคู่

TypeScript
// Jest — test.each
test.each([
[2, 3, 5],
[0, 0, 0],
[-1, 1, 0],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
Python
import pytest
def add(a: int, b: int) -> int:
return a + b
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(10, -5, 5),
])
def test_add(a, b, expected):
assert add(a, b) == expected

รวม parametrize กับ fixtures สำหรับการทดสอบแบบ combinatorial เต็มรูปแบบ:

@pytest.mark.parametrize("role", ["admin", "user", "guest"])
def test_permissions(role, user_service):
# user_service fixture inject; role parametrized
perms = user_service.get_permissions(role)
assert isinstance(perms, list)

ฝั่ง Jest ต้องต่อ chain .toBe(), .toEqual(), .toContain() ส่วน pytest ใช้ statement assert ธรรมดา แล้ว rewrite assertion ให้ตอน collection time พอ fail ก็โชว์ diff แบบละเอียดให้เลย ไม่ต้องพึ่ง matcher library

TypeScript
// Jest assertions
expect(user.name).toBe("Alice");
expect(users).toHaveLength(3);
expect(users).toContain("Bob");
expect(order.total).toBeGreaterThan(0);
expect(fn).toThrow(TypeError);
Python
# pytest — plain assert, rich diffs เมื่อ fail
assert user["name"] == "Alice"
assert len(users) == 3
assert "Bob" in users
assert order["total"] > 0
# ทดสอบ exceptions
import pytest
with pytest.raises(ValueError, match="negative"):
parse_price(-5)

เมื่อ assertion fail, pytest แสดงค่าจริง diff เต็มรูปแบบสำหรับ containers และ expression tree — รายละเอียดมากกว่า AssertionError ธรรมดามาก

วาง fixture ที่ใช้ร่วมกันไว้ใน conftest.py ระดับไดเรกทอรีไหนก็ได้ แล้ว pytest จะ discover ให้เอง ไม่ต้อง import

flowchart TD
  tests["tests/"]
  rootcf["conftest.py — fixtures for ALL tests below"]
  unit["unit/"]
  unitcf["conftest.py — fixtures only for unit/ tests"]
  testmath["test_math.py"]
  integration["integration/"]
  testapi["test_api.py"]
  tests --> rootcf
  tests --> unit
  tests --> integration
  unit --> unitcf
  unit --> testmath
  integration --> testapi
conftest.py fixture scoping by directory
tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def api_client():
from myapp import create_app
app = create_app(testing=True)
with app.test_client() as client:
yield client

monkeypatch คือ built-in fixture สำหรับ patch objects, environment variables, และ files — เทียบเท่ากับ jest.spyOn หรือ jest.mock

def test_calls_external_api(monkeypatch):
def fake_fetch(url):
return {"status": 200, "data": "mocked"}
monkeypatch.setattr("mymodule.fetch", fake_fetch)
result = mymodule.process()
assert result["status"] == 200
คู่เทียบของ `beforeEach`/`afterEach` ของ Jest สำหรับ single test ใน pytest คืออะไร?
decorator ใดที่สร้าง data-driven / parametrized tests ใน pytest?
fixture scope เริ่มต้นใน pytest คืออะไร?
วาง shared fixtures ที่ไหนเพื่อให้ pytest discover อัตโนมัติโดยไม่ต้อง import?