Kodokon kodokon.com

การทดสอบด้วย pytest: fixture, การพารามิเตอร์ไรซ์ และแนวปฏิบัติที่ดี

จัดโครงสร้างการทดสอบที่แข็งแรงด้วย pytest: การฉีด fixture, teardown ที่อิงกับ yield, การพารามิเตอร์ไรซ์แบบตาราง และกับดักของ scope ที่กว้างเกินไป

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

pytest ตั้งอยู่บนแนวคิดที่ทรงพลังสองข้อ การค้นหาตามข้อตกลง (ไฟล์ test_*.py และฟังก์ชัน test_*) และ assert ธรรมดา ๆ ซึ่งการทำ introspection ของมันจะแสดงค่าจริงของทั้งสองฝั่งเมื่อการทดสอบล้มเหลว - ไม่ต้องท่องจำ assertEqual กับสารพัดรูปแบบอีกสามสิบตัวของมันอีกต่อไป มีเครื่องมือสองอย่างที่ควรรู้จักตั้งแต่ตอนนี้ pytest.raises ใช้ตรวจว่ามี exception ถูกโยนออกมาจริง และ pytest.approx ใช้เปรียบเทียบเลขทศนิยมโดยไม่ต้องไปต่อสู้กับการปัดเศษแบบไบนารี

PYTHON
import pytest

def divide(a: float, b: float) -> float:
    return a / b

def test_nominal() -> None:
    assert divide(8, 4) == 2.0

def test_zero_division() -> None:
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

def test_floats() -> None:
    assert 0.1 + 0.2 == pytest.approx(0.3)
หากไม่มี approx นิพจน์ 0.1 + 0.2 == 0.3 จะเป็นเท็จในเลขทศนิยมแบบไบนารี

Fixture คือ dependency injection ของ pytest ประกาศพารามิเตอร์ที่มีชื่อตรงกับ fixture แล้ว pytest จะสร้างมันขึ้นมาและฉีดเข้าไปให้ ส่วน yield แยกขั้นตอนเตรียมการออกจากขั้นตอนเก็บกวาด - โค้ดที่อยู่หลังมันจะรัน แม้ว่าการทดสอบจะล้มเหลว พารามิเตอร์ scope (function โดยค่าเริ่มต้น, module, session) ควบคุมอายุของอ็อบเจกต์ จงวาง fixture ที่ใช้ร่วมกันไว้ใน conftest.py มันจะพร้อมใช้งานทั่วทั้งโฟลเดอร์โดยไม่ต้อง import สักบรรทัดเดียว

PYTHON
import sqlite3

import pytest

@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    conn.close()

def test_insert(db) -> None:
    db.execute("INSERT INTO users VALUES ('Ada')")
    query = "SELECT name FROM users"
    rows = db.execute(query).fetchall()
    assert rows == [("Ada",)]
conn.close() รันหลังการทดสอบ ไม่ว่ามันจะผ่านหรือล้มเหลว

@pytest.mark.parametrize เปลี่ยนการทดสอบหนึ่งตัวให้เป็นตารางของกรณีทดสอบ แต่ละทูเพิลกลายเป็นการทดสอบอิสระที่มีชื่อและกรองแยกได้ทีละตัว ซ้อน decorator parametrize หลายตัวเข้าด้วยกันเพื่อให้ได้ผลคูณคาร์ทีเซียนของพารามิเตอร์ จงตั้ง id ที่อ่านง่ายให้กรณีทดสอบของคุณผ่าน pytest.param(..., id='...') - รายงานความล้มเหลวควรเข้าใจได้โดยไม่ต้องเปิดไฟล์ต้นฉบับ

PYTHON
import pytest

def slugify(text: str) -> str:
    return "-".join(text.lower().split())

@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("Hello World", "hello-world"),
        ("  Python  rocks ", "python-rocks"),
        pytest.param("", "", id="empty"),
    ],
)
def test_slugify(raw: str, expected: str) -> None:
    assert slugify(raw) == expected
สามกรณี สามการทดสอบที่แยกจากกันในรายงาน
BASH
pytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5
--lf รันซ้ำเฉพาะตัวที่ล้มเหลว ส่วน --durations เผยการทดสอบที่ช้า

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. pytest รู้ได้อย่างไรว่าต้องจ่าย fixture ตัวไหนให้การทดสอบ?
    • ด้วย decorator @inject ที่วางไว้บนการทดสอบ
    • ด้วยลำดับที่ fixture ถูกประกาศไว้ในไฟล์
    • ด้วย type annotation ของพารามิเตอร์
    • ด้วยชื่อของพารามิเตอร์ ถ้ามันตรงกับ fixture ที่มองเห็นได้ (ในโมดูลหรือ conftest.py) pytest จะสร้างมันแล้วฉีดเข้าไป
  2. ใน fixture โค้ดที่วางไว้หลัง yield มีไว้ทำอะไร?
    • มันคือขั้นตอนเก็บกวาด มันรันหลังการทดสอบ แม้ว่าการทดสอบจะล้มเหลว
    • มันไม่เคยรันเลย yield จบการทำงานของ fixture
    • มันให้ค่าสำรองถ้าขั้นตอนเตรียมการล้มเหลว
    • มันรันเฉพาะเมื่อการทดสอบล้มเหลวเท่านั้น
  3. คำสั่ง pytest --lf ทำอะไร?
    • มันแสดงรายการ fixture ที่มีอยู่ในโปรเจกต์
    • มันรันซ้ำเฉพาะการทดสอบที่ล้มเหลวในการรันครั้งล่าสุด
    • มันหยุดเซสชันที่ความล้มเหลวแรกที่พบ