Test Python ด้วย Pytest
pytest คือ testing framework สำหรับการเขียน Test Code เพื่อทดสอบ Python script ที่เราเขียนขึ้นมา ซึ่ง pytest สามารถทำได้ตั้งแต่ Unit test, Integration Test ไปจนถึง End-2-End test
Pytest vs Unittest
ใน Python Standard Library จะมี Unittest มาให้ใช้อยู่แล้วจริงๆเราสามารถเขียน Test ได้โดยเลย โดยที่ไม่ต้องติดต้ังอะไรเลย แต่ Unittest จะเหมือนกับการใช้งาน JUnit หรือ XUnit ที่ออกแบบมาสำหรับการทำ Unit Test ซึ่งเหมาะสำหรับผู้ที่ต้องการทำ Unit Test และเคยใช้งาน JUnit หรือ XUnit มาอยู่แล้ว
ส่วน pytest นั้นจะสามารถทำ Test ได้ตั้งแต่ Unit Test ไปจนถึง End-2-End Test และยังมีความสามารถที่มากกว่า Unittest เช่น การใช้ Fixture และ Parameterize
ติดตั้ง Pytest
ก่อนจะใช้งาน pytest เราต้อง ติดตั้ง pytest เข้ามาก่อนโดยเราจะติดตั้งด้วย pip แบบนี้
$ pip install -U pytest
หลังจากติดตั้ง pytest เรียบร้อยแล้วให้เราลองสร้าง Python script ที่ต้องถูกทดสอบขึ้นมาก่อนโดยเราจะใช้ว่า calculator.py
def add(a, b):
return a + b
Test script ขึ้นมาก่อนโดยจะตั้งชื่อไฟล์ว่า test_scripts.py
Pytest จะเลือก run ไฟล์ที่นำหน้าด้วย
test_
หรือลงท้ายด้วย_test.py
import pytest
from calculator import add
def test_add_number():
assert add(5, 5) == 10
def test_add_negative_number():
assert add(5, -3) == 2
ลองรัน Test ด้วยคำสั่ง
$ pytest
ถ้าต้องการระบุชื่อ Test script ที่ต้องการ run เราจะใช้คำสั่ง
$ pytest test_scripts.py
ถ้าผลลัพธ์ที่ได้ออกมาถูกต้องเราจะได้ผลลัพธ์ออกมาแบบนี้่
============================== test session starts ===============================
platform darwin -- Python 3.11.6, pytest-8.2.1, pluggy-1.5.0
rootdir: /Users/demo/Documents/Workshop/pytest-demo
plugins: anyio-4.0.0, time-machine-2.13.0
collected 2 items
test_scripts.py .. [100%]
=============================== 2 passed in 0.01s ================================
Pytest Marker
เราสามารถจัดกลุ่มของ Test ด้วย @pytest.mark
แล้วตามด้วยชื่อของ group
import pytest
@pytest.mark.great
def test_greater_than():
n = 150
assert n > 100
@pytest.mark.great
def test_greater_than_or_equal():
n = 100
assert n >= 100
@pytest.mark.others
def test_less_than():
n = 90
assert n < 100
ในตัวอย่างเราจะ run Test 2 case แรกพร้อมกันเนื่องจากเป็นการทดสอบในกรณีเดียวกัน(num >= 100)
เวลาจะ run Test เราสามารถเลือก group ที่ต้องการ run ด้วยการใส่ -m
เข้าไปแบบนี้
$ pytest -m great
Pytest Fixture
ในการทดสอบที่ต้องมีการ Integration กับระบบอื่นๆเช่นดึงข้อมูลจาก Database หรือส่ง Email เราจะใช้ Fixture ในการสร้าง Setup(คำสั่งตอนเริ่มต้น) และ Tear down(คำสั่งตอนก่อนจบ Test)
Fixture จะคล้ายๆกับ Mark แต่จะเป็นการจัดกลุ่มของ Test และกำหนดค่าเริ่มต้น(Setup)แบบนี้
import pytest
@pytest.fixture
def input_value():
input = 39
return input
def test_divisible_by_3(input_value):
assert input_value % 3 == 0
def test_divisible_by_6(input_value):
assert input_value % 6 == 0
ในตัวอย่างนี้เราจะเลือก run test 2 ตัวคือ test_divisible_by_3
และ test_divisible_by_6
โดยใช้คำสั่ง (-k
จะเลือก run เฉพาะ Test ที่มีคำส่า divisible
อยู่ในชื่อ)
$ pytest -k divisible
ซึ่งจะมีการเรียก method input_value ก่อนจะเข้าไปทำงานใน Test case แต่ละตัว จะเห็นว่าเราสามารถกำหนดค่าเริ่มต้นได้ใน method input_value ที่เดียว
Pytest Parameterize
Pytest สามารถใส่ Parameter เข้าไปใน Test ได้ทำให้เราเขียน Test ครั้งเดียวแล้ว re-use ใช้หลายๆครั้งได้ หรือถ้าเรามี Data สำหรับการทดสอบหลายๆตัว เราก็สามารถส่งเข้าไปใน Parameter ของ Test Code ได้ ยกตัวอย่างเช่น
-
เริ่มต้นจากการเขียน Function add ที่เราต้องการทดสอบขึ้นมาก่อน
def add(a, b): return a + b
-
เขียน Unittest เพื่อทดสอบ Function add ที่เราได้สร้างขึ้น โดยจะใส่ Parameter เข้าไปด้วย
import pytest from calculator import add @pytest.mark.parametrize("a, b,expected", [(3, 5, 8), (2, -4, -2), (-2, -3, -5)]) def test_add(a, b, expected): assert add(a, b) == expected
ในตัวอย่างนี้เป็นการส่ง patameter เข้าไป 3 ชุดคือ
- 3 + 5 = 8
- 2 + (-4) = -2
- (-2) + (-3) = -5
ซึ่งสมาชิก 2 ตัวแรกใน tupleจะถูกส่งเข้าไปเป็น parameter a และ b ใน function add และสมาชิกตัวหลังสุดจะเป็นค่าที่เรา expected(คำตอบที่คาดว่าจะได้)
-
run pytest เพื่อทดสอบ
$ pytest
เราจะได้ผลลัพธ์ออกมาแบบนี้
============================== test session starts =============================== platform darwin -- Python 3.11.6, pytest-8.2.1, pluggy-1.5.0 rootdir: /Users/demo/Documents/Workshop/pytest-demo plugins: anyio-4.0.0, time-machine-2.13.0 collected 3 items test_scripts.py ... [100%] =============================== 3 passed in 0.01s ================================
Pytest Plugins
เราสามารถติดตั้ง plugins เข้ามาใน Pytest เพื่อให้ Pytest มีความสามารถมากขึ้น ยกตัวอย่างเช่น
Pytest Mock
เราสามารถใช้ Pytest-Mock เพื่อจำลองข้อมูลขึ้นมาโดยที่เราไม่ต้องใส่เข้าไป ซึ่งการใช้ Mock สำหรับ pytest นั้นจะสามารถทำ Test Double ได้ทั้ง
- Stub
- Fake
- Dummy
- Mock
- Spy
ซึ่งการทำ Test Double มีหลายวิธีตามรายการด้านบนแต่ทุกตัวล้วนมีจุดประสงค์เดียวกันคือ เขียน Test เฉพาะสิ่งที่กำลังสนใจ ส่วนอื่นๆรอบข้างก็สร้าง Code จำลองขึ้นมาทั้งหมด ตัวอย่างการใช้ Mock มีดังนี้
-
ติดต้ัง Mock สำหรับ Pytest เข้ามาก่อน
$ pip install pytest-mock
-
หลังจากนั้น Mock สิ่งที่เราต้องการขึ้นมา
import os class UnixFS: @staticmethod def rm(filename): os.remove(filename) def test_unix_fs(mocker): mocker.patch('os.remove') UnixFS.rm('file') os.remove.assert_called_once_with('file')
ในตัวอย่างนี้เราใช้ Mock(ชื่อ library) เพื่อสร้าง Class จำลองขึ้นมา ซึ่ง Class นี้ชื่อ UnixFS และ class นี้จะมี Static Method ชื่อ rm และ method rm จะรับชื่อไฟล์เข้ามา เวลา test เราก็จะสามารถเรียก
UnixFS.rm()
ใน function test ของเราได้เลย โดยที่ไม่ต้องเสียเวลาไปสร้าง Class UnixFS ขึ้นมาจริงๆโดยที่เราจะสร้าง Function Foo ขึ้นมา และนอกจากนี้เรายังสามารถใช้
mock.patch
สำหรับสร้าง function ปลอมๆขึ้นมา เหมือนในตัวอย่างos.remove()
เป็นคำสั่งที่ใช้ลบไฟล์ที่มีอยู่ในเครื่อง ถ้าเราใช้ function ของจริงไฟล์ที่อยู่ในเครื่องเราจะหายไป แต่ในกรณีนี้เราจะใช้mock.patch
ทำให้os.remove
กลายเป็นของปลอมหลังจากนั้นเราจะใช้
os.remove.assert_called_once_with()
เพื่อตรวจสอบว่ามีการเรียกใช้os.remove
โดยส่ง parameter เป็นชื่อfile
เข้ามารึเปล่า
Pytest Coverage
Code Coverage คือตัววัดที่จะบ่งชี้ถึงคุณภาพของการทดสอบ เป็นตัววัดที่จะบอกว่า Code ของเราถูกทดสอบไปกี่เปอร์เซ็นต์แล้ว วิธีการสร้าง Code Coverage Report มีดังนี้
-
ติดตั้ง Library ชื่อ Coverage
$ pip install coverage
-
หลังจากนั้นให้เปลี่ยนคำสั่งที่ใช้ในการ run จาก
pytest
เป็นcoverage run
แบบนี้$ coverage run test_scripts.py
เราสามารถใช้ parameters ต่างๆของ pytest ต่อท้ายได้เลย เช่น
$ coverage run -m great
-
เรียกดู Code Coverage Report ด้วยคำสั่ง
$ coverage report -m
-
ถ้าต้องการ Export ออกเป็น HTML ให้เราใช้คำสั่ง
$ coverage html
หลังจากนั้นเราจะได้ไฟล์
index.html
อยู่ใน Folder ชื่อว่าhmtlcov
คุณจะได้ report ออกมาดังรูป
หวังว่าผู้อ่านจะได้นำเอา Pytest ไปลองเขียน Test กัน แลัวพบกันใหม่ในบทความถัดๆไปนะครับ