pytest — เชิงลึก
Playground note: pytest ต้องการ binary
pytestและ test discovery context — รัน locally ด้วยpytestcode blocks ด้านล่างได้รับการตรวจสอบด้วยpython3ในส่วนที่ทำได้; การรัน test เต็มรูปแบบต้องใช้ environment ใน local
pytest vs Jest: แผนที่แนวคิด
หัวข้อที่มีชื่อว่า “pytest vs Jest: แผนที่แนวคิด”| แนวคิด Jest | คู่เทียบ pytest |
|---|---|
describe block | การจัดกลุ่มด้วย module หรือ class (optional) |
it / test | ฟังก์ชันที่ชื่อขึ้นต้นด้วย test_* |
beforeEach / afterEach | fixture ที่มี yield |
beforeAll / afterAll | fixture ที่มี 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 |
Fixtures: พลังพิเศษของ pytest
หัวข้อที่มีชื่อว่า “Fixtures: พลังพิเศษของ pytest”fixture คือฟังก์ชันที่ครอบด้วย @pytest.fixture แล้ว pytest จะ inject เข้าไปให้ทุก test ที่ประกาศชื่อไว้เป็น parameter บทบาทเทียบได้กับ beforeEach ของ Jest แต่ทำได้มากกว่า เพราะ fixture compose กันได้ คือ fixture ตัวหนึ่ง depend บน fixture ตัวอื่นต่อไปอีกทอดได้
// Jest — beforeEach for setupdescribe("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(); });});# pytest — fixture with yield (setup + teardown)import pytest
@pytest.fixturedef db(): connection = {"connected": True, "data": {}} yield connection # test ได้รับค่านี้ connection["connected"] = False # teardown หลัง yield
@pytest.fixturedef 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:
pip install pytestpytest test_users.py -vFixture scopes
หัวข้อที่มีชื่อว่า “Fixture scopes”Fixtures ค่าเริ่มต้นคือ scope="function" (สร้างใหม่ต่อ test) scopes ที่กว้างกว่าหลีกเลี่ยง setup ที่ cost สูง:
@pytest.fixture(scope="module") # ครั้งเดียวต่อ test moduledef db_connection(): conn = create_db_connection() yield conn conn.close()
@pytest.fixture(scope="session") # ครั้งเดียวตลอดการรัน pytestdef auth_token(): return get_test_token()| Scope | สร้างเมื่อไหร่ |
|---|---|
function (ค่าเริ่มต้น) | ก่อนทุก test |
class | ครั้งเดียวต่อ test class |
module | ครั้งเดียวต่อ .py file |
session | ครั้งเดียวต่อการเรียก pytest |
parametrize: data-driven tests
หัวข้อที่มีชื่อว่า “parametrize: data-driven tests”@pytest.mark.parametrize คือ it.each ของ Jest — รัน test เดียวกันด้วย input/output หลายคู่
// Jest — test.eachtest.each([ [2, 3, 5], [0, 0, 0], [-1, 1, 0],])("add(%i, %i) = %i", (a, b, expected) => { expect(add(a, b)).toBe(expected);});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)Assertions: assert ธรรมดาพร้อม introspection
หัวข้อที่มีชื่อว่า “Assertions: assert ธรรมดาพร้อม introspection”ฝั่ง Jest ต้องต่อ chain .toBe(), .toEqual(), .toContain() ส่วน pytest ใช้ statement assert ธรรมดา แล้ว rewrite assertion ให้ตอน collection time พอ fail ก็โชว์ diff แบบละเอียดให้เลย ไม่ต้องพึ่ง matcher library
// Jest assertionsexpect(user.name).toBe("Alice");expect(users).toHaveLength(3);expect(users).toContain("Bob");expect(order.total).toBeGreaterThan(0);expect(fn).toThrow(TypeError);# pytest — plain assert, rich diffs เมื่อ failassert user["name"] == "Alice"assert len(users) == 3assert "Bob" in usersassert order["total"] > 0
# ทดสอบ exceptionsimport pytestwith pytest.raises(ValueError, match="negative"): parse_price(-5)เมื่อ assertion fail, pytest แสดงค่าจริง diff เต็มรูปแบบสำหรับ containers และ expression tree — รายละเอียดมากกว่า AssertionError ธรรมดามาก
conftest.py: shared fixtures
หัวข้อที่มีชื่อว่า “conftest.py: shared fixtures”วาง 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
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 clientmonkeypatch: built-in mock
หัวข้อที่มีชื่อว่า “monkeypatch: built-in mock”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