13 เคล็ดลับในการใช้ PyTest
การทดสอบหน่วยเป็นทักษะที่สำคัญมากสำหรับการพัฒนาซอฟต์แวร์ มีไลบรา รี่Python ดีๆ บางตัวที่ช่วยเราเขียนและเรียกใช้การทดสอบหน่วย เช่นNoseและUnittest แต่ที่ฉันชอบคือPyTest
ฉันเพิ่งอ่านเอกสารของ PyTest อย่างละเอียดเพื่อเรียนรู้เกี่ยวกับคุณสมบัติในเชิงลึกมากขึ้น
ต่อไปนี้คือรายการคุณลักษณะที่ไม่ชัดเจนบางอย่างที่ฉันพบว่ามีประโยชน์และจะเริ่มรวมเข้ากับเวิร์กโฟลว์การทดสอบของฉันเอง ฉันหวังว่าจะมีสิ่งใหม่ในรายการนี้ที่คุณยังไม่รู้...
ข้อมูลโค้ดทั้งหมดในโพสต์นี้มีอยู่ในe4ds -snippets GitHub repository
เคล็ดลับทั่วไปสำหรับการทดสอบการเขียน
1. วิธีการเขียน unit-test ที่ดี
ตกลง ดังนั้นสิ่งนี้ไม่เฉพาะเจาะจงกับไลบรารี PyTest แต่คำแนะนำแรกคือดูเอกสารประกอบของ PyTestเกี่ยวกับการจัดโครงสร้างการทดสอบหน่วยของคุณ มันคุ้มค่าที่จะอ่านอย่างรวดเร็ว
การทดสอบที่ดีควรตรวจสอบพฤติกรรมที่คาดหวังและสามารถเรียกใช้โดยอิสระจากโค้ดอื่นๆ คือภายในการทดสอบควรเป็นรหัสทั้งหมดที่จำเป็นในการตั้งค่าและเรียกใช้ลักษณะการทำงานที่จะทดสอบ
สามารถสรุปได้เป็นสี่ขั้นตอน:
- จัดเรียง — ดำเนินการตั้งค่าที่จำเป็นสำหรับการทดสอบ เช่นการกำหนดอินพุต
- Act — เรียกใช้ฟังก์ชันที่คุณต้องการทดสอบ
- ยืนยัน — ตรวจสอบว่าเอาต์พุตของฟังก์ชันเป็นไปตามที่คาดไว้
- การล้าง — (ไม่บังคับ) ล้างอาร์ติแฟกต์ใดๆ ที่สร้างขึ้นจากการทดสอบ เช่นไฟล์เอาต์พุต
# example function
def sum_list(my_list):
return sum(my_list)
# example test case
def test_sum_list():
# arrange
test_list = [1, 2, 3]
# act
answer = sum_list(test_list)
# Assert
assert answer == 6
https://docs.pytest.org/en/7.1.x/explanation/anatomy.html
2. ข้อยกเว้นการทดสอบ
โดยปกติ สิ่งแรกที่เรานึกถึงการทดสอบคือผลลัพธ์ที่คาดไว้ของฟังก์ชันเมื่อทำงานสำเร็จ
อย่างไรก็ตาม สิ่งสำคัญคือต้องตรวจสอบพฤติกรรมของฟังก์ชันเมื่อเกิดข้อยกเว้น โดยเฉพาะอย่างยิ่งหากคุณทราบว่าอินพุตประเภทใดควรเพิ่มข้อยกเว้นบางประการ
คุณสามารถทดสอบข้อยกเว้นโดยใช้ตัวpytest.raises
จัดการบริบท
ตัวอย่างเช่น:
import pytest
def divide(a, b):
"""Divide to numbers"""
return a/b
def test_zero_division():
with pytest.raises(ZeroDivisionError):
divide(1,0)
def test_type_error():
with pytest.raises(TypeError):
divide("abc",10)
3. ทดสอบการบันทึก/การพิมพ์
PyTest ให้คุณทดสอบคำสั่งการพิมพ์และการบันทึกในโค้ดของคุณ
มีการติดตั้ง PyTest ในตัวสองตัว ได้แก่capsysและcaplogซึ่งสามารถใช้เพื่อติดตามข้อมูลที่พิมพ์ไปยังเทอร์มินัลโดยฟังก์ชัน
ทดสอบผลลัพธ์การพิมพ์
def printing_func(name):
print(f"Hello {name}")
def test_printing_func(capsys):
printing_func(name="John")
# use the capsys fixture to record terminal output
output = capsys.readouterr()
assert output.out == "Hello John\n"
ทดสอบเอาต์พุตการบันทึก
import logging
def logging_func():
logging.info("Running important function")
# some more code...
logging.info("Function completed")
def test_logging_func(caplog):
# use the caplog fixture to record logging records
caplog.set_level(logging.INFO)
logging_func()
records = caplog.records
# first message
assert records[0].levelname == 'INFO'
assert records[0].message == "Running important function"
# second message
assert records[1].levelname == 'INFO'
assert records[1].message == "Function completed"
4. การทดสอบการลอยตัว
เลขคณิตที่เกี่ยวข้องกับทศนิยมอาจ ทำให้เกิด ปัญหาใน Python
ตัวอย่างเช่น ฟังก์ชันง่ายๆ นี้ทำให้เกิดข้อผิดพลาดที่น่าสงสัย:
def subtract_floats(a,b):
return a - b
def test_substract_floats():
assert subtract_floats(1.2, 1.0) == 0.2
ไม่มีอะไรผิดปกติกับตรรกะของฟังก์ชันและไม่ควรทำให้กรณีทดสอบนี้ล้มเหลว
เพื่อต่อสู้กับข้อผิดพลาดในการปัดเศษของทศนิยมในการทดสอบ คุณสามารถใช้ฟังก์ชันประมาณ
import pytest
def test_substract_floats():
assert subtract_floats(1.2, 1.0) == pytest.approx(0.2)
หมายเหตุ คุณสามารถใช้approx
ฟังก์ชันนี้กับอาร์เรย์แบบ numpy สิ่งนี้มีประโยชน์เมื่อเปรียบเทียบอาร์เรย์และดาต้าเฟรม
ตัวอย่างเช่น:
import pytest
import numpy as np
np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == pytest.approx(np.array([0.3, 0.6]))
เคล็ดลับที่ช่วยประหยัดเวลา⏳
5. ประหยัดเวลาโดยเรียกใช้การทดสอบบางอย่างเท่านั้น
การทดสอบการทำงานควรช่วยเวิร์กโฟลว์ของคุณและไม่เป็นอุปสรรค ชุดการทดสอบที่ใช้เวลานานอาจทำให้คุณทำงานช้าลงและทำให้คุณไม่สามารถทำการทดสอบเป็นประจำได้
โดยปกติ คุณไม่จำเป็นต้องเรียกใช้ชุดการทดสอบทั้งหมดทุกครั้งที่คุณทำการเปลี่ยนแปลง โดยเฉพาะอย่างยิ่งหากคุณกำลังทำงานในส่วนเล็กๆ ของโค้ดเบสเท่านั้น
ดังนั้นจึงสะดวกที่จะเรียกใช้ชุดย่อยของการทดสอบที่เกี่ยวข้องกับโค้ดที่คุณกำลังทำงานอยู่
PyTest มาพร้อมกับตัวเลือกสองสามตัวสำหรับเลือกการทดสอบที่จะเรียกใช้:
การใช้-k
ธง
คุณสามารถใช้-k
แฟล็กเมื่อเรียกใช้ PyTest เพื่อเรียกใช้การทดสอบที่ตรงกับสตริงย่อยที่กำหนดเท่านั้น
ตัวอย่างเช่น หากคุณมีการทดสอบต่อไปนี้:
def test_preprocess_categorical_columns():
...
def test_preprocess_numerical_columns():
...
def test_preprocess_text():
...
def test_train_model():
...
# run first test only
pytest -k categorical
# run first three tests only
pytest -k preprocess
# run first two tests only
pytest -k "preprocess and not text"
เรียกใช้การทดสอบในไฟล์ทดสอบเดียว
หากการทดสอบของคุณแบ่งออกเป็นหลายๆ ไฟล์ คุณสามารถเรียกใช้การทดสอบจากไฟล์เดียวโดยระบุชื่อไฟล์อย่างชัดเจนเมื่อเรียกใช้ PyTest:
# only run tests defined in 'tests/test_file1.py' file
pytest tests/test_file1.py
คุณยังสามารถใช้ 'เครื่องหมาย' pytest เพื่อทำเครื่องหมายการทดสอบบางอย่าง สิ่งนี้มีประโยชน์สำหรับการทำเครื่องหมายการทดสอบ 'ช้า' ซึ่งคุณสามารถยกเว้นได้ด้วยการ-m
ตั้งค่าสถานะ
ตัวอย่างเช่น.
import time
import pytest
def my_slow_func():
# some long running code...
time.sleep(5)
return True
@pytest.mark.slow
def test_my_slow_func():
assert my_slow_func()
หลังจากใช้@pytest.mark.slow
มัณฑนากร เราสามารถยกเว้นการทดสอบนี้ทุกครั้งที่ใช้-m
แฟล็ก:
# exclude running tests marked as slow
pytest -m "not slow"
import sys
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function():
...
6. เรียกใช้การทดสอบที่ล้มเหลวซ้ำเท่านั้น
เมื่อคุณเรียกใช้ชุดการทดสอบทั้งหมด คุณอาจพบว่ามีการทดสอบจำนวนเล็กน้อยที่ล้มเหลว
เมื่อคุณดีบั๊กปัญหาและอัปเดตโค้ดแล้ว แทนที่จะเรียกใช้ชุดทดสอบทั้งหมดอีกครั้ง คุณสามารถใช้--lf
แฟล็กเพื่อรันเฉพาะการทดสอบที่ล้มเหลวในการรันครั้งล่าสุด
คุณสามารถตรวจสอบว่าโค้ดที่อัปเดตผ่านการทดสอบเหล่านี้ก่อนที่จะเรียกใช้ชุดการทดสอบทั้งหมดอีกครั้ง
# only run tests which failed on last run
pytest --lf
# run all tests but run failed tests first
pytest --ff
เคล็ดลับที่ช่วยให้คุณไม่ต้องเขียนโค้ด
7. การทดสอบ Paramtarising
เมื่อคุณต้องการทดสอบอินพุตที่แตกต่างกันหลายรายการสำหรับฟังก์ชันเฉพาะ เป็นเรื่องปกติที่ผู้คนจะเขียนคำสั่งยืนยันหลายรายการภายในฟังก์ชันทดสอบ ตัวอย่างเช่น:
def remove_special_characters(input_string):
return re.sub(r"[^A-Za-z0-9]+", "", input_string)
def test_remove_special_characters():
assert remove_special_characters("hi*?.") == "hi"
assert remove_special_characters("f*()oo") == "foo"
assert remove_special_characters("1234bar") == "bar"
assert remove_special_characters("") == ""
import pytest
@pytest.mark.parametrize(
"input_string,expected",
[
("hi*?.", "hi"),
("f*()oo", "foo"),
("1234bar", "1234bar"),
("", ""),
],
)
def test_remove_special_characters(input_string, expected):
assert remove_special_characters(input_string) == expected
8. เรียกใช้การทดสอบจากเอกสาร
เคล็ดลับเด็ดอีกอย่างคือการกำหนดและเรียกใช้การทดสอบโดยตรงจากเอกสาร
คุณสามารถกำหนดกรณีทดสอบในสตริงเอกสารได้ดังนี้:
def add(a, b):
"""Add two numbers
>>> add(2,2)
4
"""
return a + b
pytest --doctest-modules
ฉันพบว่ามันใช้งานได้ดีสำหรับฟังก์ชันที่มีโครงสร้างข้อมูล 'แบบง่าย' เป็นอินพุตและเอาต์พุต แทนที่จะเขียนการทดสอบแบบสมบูรณ์ซึ่งเพิ่มรหัสเพิ่มเติมให้กับชุดการทดสอบ
https://docs.pytest.org/en/7.1.x/how-to/doctest.html#how-to-run-doctests
9. ติดตั้ง pytest ในตัว
PyTest มีฟิกซ์เจอร์ในตัวจำนวนหนึ่งที่มีประโยชน์มาก
เรากล่าวถึงการแข่งขันเหล่านี้สั้นๆ สองสามรายการในเคล็ดลับหมายเลข 3 — capsys และ caplog — แต่สามารถดูรายการทั้งหมดได้ที่นี่:https://docs.pytest.org/en/stable/reference/fixtures.html#built-in-fixtures
การติดตั้งเหล่านี้สามารถเข้าถึงได้โดยการทดสอบของคุณเพียงแค่เพิ่มเป็นอาร์กิวเมนต์ให้กับฟังก์ชันการทดสอบ
การติดตั้งในตัวที่มีประโยชน์มากที่สุดสองรายการในความคิดของฉันคือการrequest
ติดตั้งและการtmp_path_factory
ติดตั้ง
คุณสามารถอ่านบทความของฉันเกี่ยวกับการใช้request
ฟิกซ์เจอร์เพื่อใช้ฟิกซ์เจอร์ในการทดสอบพารามิเตอร์ได้ ที่นี่
tmp_path_factory
สามารถใช้ฟิกซ์เจอร์เพื่อสร้างไดเร็กทอรีชั่วคราวสำหรับรันการทดสอบ ตัวอย่างเช่น หากคุณกำลังทดสอบฟังก์ชันที่ต้องบันทึกไฟล์ไปยังไดเร็กทอรีหนึ่งๆ
https://docs.pytest.org/en/stable/reference/fixtures.html#built-in-fixtures
https://docs.pytest.org/en/7.1.x/how-to/tmp_path.html#the-tmp-path-factory-fixture
เคล็ดลับเพื่อช่วยในการแก้ไขจุดบกพร่อง
10. เพิ่มความฟุ่มเฟือยของการทดสอบ
เอาต์พุตเริ่มต้นของ PyTest อาจค่อนข้างน้อย หากการทดสอบของคุณล้มเหลว การเพิ่มปริมาณข้อมูลที่ให้ไว้ในเอาต์พุตของเทอร์มินัลอาจเป็นประโยชน์
สามารถเพิ่มได้โดยใช้แฟล็กรายละเอียด-vv
# increase the amount of information provided by PyTest in the terminal output
pytest -vv
11. แสดงระยะเวลาของการทดสอบ
หากชุดทดสอบของคุณใช้เวลานานในการรัน คุณอาจต้องการทำความเข้าใจว่าการทดสอบใดใช้เวลานานที่สุดในการรัน จากนั้นคุณสามารถลองและเพิ่มประสิทธิภาพการทดสอบเหล่านี้หรือใช้เครื่องหมายเพื่อแยกการทดสอบเหล่านี้ตามที่แสดงให้เห็นด้านบน
คุณสามารถดูได้ว่าการทดสอบใดใช้เวลานานที่สุดในการเรียกใช้โดยใช้--durations
แฟล็ก
คุณต้องส่งแฟล็กคำฟุ่มเฟือยเพื่อแสดงรายงานระยะเวลาทั้งหมด
# show top 5 longest running tests
pytest --durations=5 -vv
12. แสดงผลคำสั่งการพิมพ์ในรหัสของคุณ
บางครั้งคุณจะมีคำสั่งการพิมพ์ในซอร์สโค้ดของคุณเพื่อช่วยในการแก้ไขจุดบกพร่องของฟังก์ชันของคุณ
ตามค่าเริ่มต้น Pytest จะไม่แสดงผลลัพธ์ของคำสั่งการพิมพ์เหล่านี้หากการทดสอบผ่าน
คุณสามารถแทนที่ลักษณะการทำงานนี้ได้โดยใช้-rP
แฟล็ก
def my_function_with_print_statements():
print("foo")
print("bar")
return True
def test_my_function_with_print_statements():
assert my_function_with_print_statements()
# run tests but show all printed output of passing tests
pytest -rP
13. กำหนด ID ให้กับการทดสอบแบบพาราเมตริก
ปัญหาหนึ่งที่อาจเกิดขึ้นกับการเรียกใช้การทดสอบแบบพาราเมตริกคือการทดสอบทั้งหมดจะปรากฏด้วยชื่อเดียวกันในเอาต์พุตของเทอร์มินัล แม้ว่าพวกเขาจะทดสอบทางเทคนิคสำหรับพฤติกรรมที่แตกต่างกัน
คุณสามารถเพิ่ม ID ให้กับการทดสอบแบบกำหนดพารามิเตอร์เพื่อให้การทดสอบแบบกำหนดพารามิเตอร์แต่ละรายการมีชื่อไม่ซ้ำกัน ซึ่งจะช่วยระบุการทดสอบได้ นอกจากนี้ยังเพิ่มความสามารถในการอ่านการทดสอบของคุณ เนื่องจากคุณสามารถระบุอย่างชัดเจนเกี่ยวกับสิ่งที่คุณกำลังพยายามทดสอบ
มีสองตัวเลือกที่นี่สำหรับการเพิ่มรหัสในการทดสอบของคุณ:
ตัวเลือกที่ 1 : id
ข้อโต้แย้ง
ใช้ตัวอย่างพารามิเตอร์จากเคล็ดลับหมายเลข 7 ซ้ำ:
@pytest.mark.parametrize(
"input_string,expected",
[
("hi*?.", "hi"),
("f*()oo", "foo"),
("1234bar", "1234bar"),
("", ""),
],
ids=[
"remove_special_chars_from_end",
"remove_special_chars_from_middle",
"ignore_numbers",
"no_input",
],
)
def test_remove_special_characters(input_string, expected):
assert remove_special_characters(input_string) == expected
หรือใช้pytest.param
กระดาษห่อหุ้ม:
@pytest.mark.parametrize(
"input_string,expected",
[
pytest.param("hi*?.", "hi", id="remove_special_chars_from_end"),
pytest.param("f*()oo", "foo", id="remove_special_chars_from_middle"),
pytest.param("1234bar", "1234bar", id="ignore_numbers"),
pytest.param("", "", id="no_input"),
],
)
def test_remove_special_characters(input_string, expected):
assert remove_special_characters(input_string) == expected
https://docs.pytest.org/en/stable/example/parametrize.html#different-options-for-test-ids
บทสรุป
PyTest เป็นกรอบการทดสอบที่ยอดเยี่ยมพร้อมคุณสมบัติที่มีประโยชน์มากมาย โดยทั่วไปเอกสารประกอบนั้นดีมาก และฉันขอแนะนำให้ค้นหาข้อมูลเพิ่มเติมและคุณสมบัติที่ยอดเยี่ยมอื่นๆ
ฉันหวังว่าคุณจะได้เรียนรู้สิ่งใหม่ๆ — ฉันสนใจที่จะทราบว่าคุณมีเคล็ดลับอะไรอีกบ้างสำหรับการใช้ PyTest
มีความสุขในการทดสอบ!
บทความนี้เผยแพร่ครั้งแรกที่engineeringfordatascience.com