ผลต่างระหว่างรุ่นของ "01204223/flask-testing"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
| แถว 154: | แถว 154: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | โค้ดข้างต้นจะสร้าง fixtures | + | โค้ดข้างต้นจะสร้าง fixtures สาม fixture คือ |
| − | * app - จะสร้าง flask app ใหม่ โดยปรับโหมดเป็น testing และเปลี่ยน SQLALCHEMY_DATABASE_URI | + | * app - จะสร้าง flask app ใหม่ โดยปรับโหมดเป็น testing และเปลี่ยน SQLALCHEMY_DATABASE_URI ให้เป็น database แบบ in-memory (คือเก็บฐานข้อมูลทั้งหมดในหน่วยความจำ ซึ่งจะทำงานได้รวดเร็วกว่าการสร้างฐานข้อมูลเป็นไฟล์โดยตรง) |
* client (ซึ่งจะสร้างจาก fixture app) - จะสร้าง client สำหรับทดสอบ route ต่าง ๆ | * client (ซึ่งจะสร้างจาก fixture app) - จะสร้าง client สำหรับทดสอบ route ต่าง ๆ | ||
* app_context (ซึ่งจะสร้างจาก fixture app) - สร้าง app_context สำหรับทดสอบโมเดลที่ทำงานกับฐานข้อมูล | * app_context (ซึ่งจะสร้างจาก fixture app) - สร้าง app_context สำหรับทดสอบโมเดลที่ทำงานกับฐานข้อมูล | ||
รุ่นแก้ไขเมื่อ 01:59, 27 กุมภาพันธ์ 2569
- หน้านี้เป็นส่วนหนึ่งของวิชา 01204223
เราจะทดสอบส่วน Flask backend ด้วย pytest โดยการทดสอบเราจะเป็นกึ่ง ๆ unit test ที่จะเน้นทดสอบแค่บาง api end-point แต่เนื่องจาก backend เรามีการติดต่อกับ database ผ่านทาง SQLAlchemy ซึ่ง mock ยุ่งยากมาก เราจึงจะไม่ได้ mock แต่จะสร้าง test database มาแทน ทำให้การทำงานของ test เรานั้นจะมีการติดต่อไปยังฐานข้อมูลด้วย (จึงอาจจะเป็น unit test ที่ติดต่อกับระบบอื่นๆ มากเกินไปสักนิด)
เนื้อหา
ติดตั้งและตั้งค่าเริ่มต้น
เข้าไปทำงานในไดเร็กทอรี backend
ก่อนอื่นเราจะต้อง activate virtual environment ก่อน
จากนั้นจะติดตั้ง pytest โดยเรียก
pip install pytest
ถ้าติดตั้งเรียบร้อยจะเรียก
pytest
ได้ และเห็นผลลัพธ์ที่แสดงว่าเราไม่มี test case อะไรเลย (จะมีข้อความว่า collected 0 items)
ก่อนจะทำงานต่อ ให้เก็บรายการของ packages ลงใน requirements.txt โดยสั่ง
pip freeze > requirements.txt
เพื่อที่เราจะได้ไปเทสใน github action ได้ต่อไป
ตัวอย่างเทสเคสแรก
เราจะสร้างไดเร็กทอรี backend/tests เพื่อเก็บไฟล์ test case ต่างๆ จากนั้นให้สร้างไฟล์ backend/tests/test_models.py ที่มีเทสเคสตัวอย่างดังนี้
def test_add():
assert 2+3 == 5
สังเกตว่าฟังก์ชันชื่อขึ้นต้นด้วย test และอยู่ในไฟล์ที่ชื่อขึ้นต้นด้วย test ซึ่งเป็นกฎที่ pytest ใช้ในการหา test functions
เมื่อเขียนเสร็จแล้ว ให้ลองเรียก (เรียกจากไดเร็กทอรี backend)
pytest
จะเห็นผลลัพธ์ดังนี้
============================= test session starts ============================== platform linux -- Python 3.13.7, pytest-8.3.4, pluggy-1.6.0 rootdir: /home/jittat/prog/test/flask-react-todo-start/backend collected 1 item tests/test_models.py . [100%] ============================== 1 passed in 0.01s ===============================
แสดงว่าเทสทำงานถูกต้อง ให้ลองแก้ฟังก์ชันด้านบนเป็น
def test_add():
assert 2+3 == 6
แล้วลองเรียก pytest อีกครั้ง จะเห็นว่ามีการฟ้องว่าทดสอบไม่ผ่าน
ให้ลบฟังก์ชัน test_add ออกก่อน เพราะเราจะทดลองเพิ่มฟังก์ชันเทสแบบจริงจัง
ทดสอบการตรวจสอบรหัสผ่าน
เราจะทดสอบฟังก์ชัน set_password และ check_password ที่เราเขียนไว้ในคลาส User โดยเราจะสร้าง test functions เพื่อทดสอบสองฟังก์ชันดังนี้
def test_check_correct_password():
user = User()
user.set_password("testpassword")
assert user.check_password("testpassword") == True
def test_check_incorrect_password():
user = User()
user.set_password("testpassword")
assert user.check_password("testpassworx") == False
แต่ฟังก์ชันทั้งสองยังไม่สามารถทำงานได้ เพราะว่าเราต้อง import คลาส User ให้ได้ก่อน แต่ test cases เราอยู่ในไดเร็กทอรี tests ซึ่งทำให้การสั่ง import ตรง ๆ จะทำไม่ได้
เพื่อความสะดวกในการจัดการ test และยังจำเป็นในการสร้าง test database เราจะเพิ่มโค้ดเพื่อตั้งค่าและเตรียมสภาพแวดล้อมเพื่อการทดสอบเสียก่อน
ให้สร้างไฟล์ชื่อ tests/conftest.py ซึ่งเป็นไฟล์สำหรับตั้งค่าของ pytest โดยใส่คำสั่งดังนี้
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
คำสั่งดังกล่าวจะเพิ่ม path ของ backend ลงไปใน sys.path ทำให้สามารถ import คลาส User ได้
จากนั้นให้แก้ไฟล์ tests/test_models.py ให้เป็น
from models import User
def test_check_correct_password():
user = User()
user.set_password("testpassword")
assert user.check_password("testpassword") == True
def test_check_incorrect_password():
user = User()
user.set_password("testpassword")
assert user.check_password("testpassworx") == False
แล้วทดลองเรียก pytest เพื่อทำงาน และดูผลลัพธ์การทดสอบ
ฐานข้อมูลสำหรับทดสอบ
สมมติว่าเราต้องการทดสอบโมเดล TodoItem ว่ามี comments ติดมาด้วยเวลาเราเรียกใช้ เราจำเป็นจะต้องมีข้อมูลตัวอย่างที่คงเส้นคงวาในการทดสอบ อย่างไรก็ตาม ในโค้ดปัจจุบันที่เราเขียน เมื่อเราเรียก pytest โค้ดของเราจะทำงานกับฐานข้อมูลหลักที่เราใช้ ซึ่งอาจมีค่าไม่แน่นอน ทำให้การเขียน test case ทำแทบไม่ได้ นอกจากนี้ ถ้าเราทดสอบลบหรือแก้ไข TodoItem ก็อาจจะทำให้ข้อมูลจริงเปลี่ยนไปได้ด้วย
เราจะจัดการกับปัญญหานี้โดยสร้างฐานข้อมูลสำหรับ test ขึ้นมาใหม่โดยเฉพาะเมื่อเริ่มต้นทดสอบ และจะทำลายทิ้งเมื่อทดสอบเสร็จ
เราจะใช้ fixtures ใน pytest เพื่อจัดการขั้นตอนการกำหนดค่าเริ่มต้นก่อนเทสและการเก็บกวาดหลังเทส
เราจะเพิ่มบรรทัดต่อไปนี้ลงใน conftest.py (ต่อจากโค้ดเดิมที่เพิ่ม sys.path)
import pytest
from main import app as flask_app
from models import db
@pytest.fixture
def app():
flask_app.config.update(
{
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': f'sqlite:///:memory:',
}
)
with flask_app.app_context():
db.drop_all()
db.create_all()
return flask_app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def app_context(app):
with app.app_context():
yield
โค้ดข้างต้นจะสร้าง fixtures สาม fixture คือ
- app - จะสร้าง flask app ใหม่ โดยปรับโหมดเป็น testing และเปลี่ยน SQLALCHEMY_DATABASE_URI ให้เป็น database แบบ in-memory (คือเก็บฐานข้อมูลทั้งหมดในหน่วยความจำ ซึ่งจะทำงานได้รวดเร็วกว่าการสร้างฐานข้อมูลเป็นไฟล์โดยตรง)
- client (ซึ่งจะสร้างจาก fixture app) - จะสร้าง client สำหรับทดสอบ route ต่าง ๆ
- app_context (ซึ่งจะสร้างจาก fixture app) - สร้าง app_context สำหรับทดสอบโมเดลที่ทำงานกับฐานข้อมูล
เมื่อระบุ fixture แล้ว เราสามารถระบุให้ test case เรียกใช้ (หรือทำงานภายใต้) fixture ที่ระบุได้ โดยใส่ fixture ไว้ในพารามิเตอร์ ดังจะได้ดูตัวอย่างเพิ่มเติมต่อไป
Github action
เราจะเพิ่ม github action ให้มีการรัน test ในส่วน backend เมื่อมีการ push ขึ้นไปที่ repository
เก็บกวาด requirements.txt
เราจะต้องรับประกันว่า package ที่เราจะใช้สามารถติดตั้งบน server ที่จะรันเทสได้อย่างครบถ้วน ดังนั้นเราควรจะรัน
pip freeze > requirements.txt
ใน backend (อย่าลืมเรียก activate virtual env ก่อนด้วย)
เพิ่ม action
ด้านล่างเป็นไฟล์ .github/workflows/backend-tests.yml
name: Backend Tests
on:
push:
paths:
- 'backend/**'
- '.github/workflows/backend-tests.yml'
pull_request:
paths:
- 'backend/**'
- '.github/workflows/backend-tests.yml'
workflow_dispatch:
jobs:
test-backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: 'backend/requirements.txt'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run pytest
run: pytest -q
ให้ add ไฟล์ที่ทำทั้งหมด รวมทั้งไฟล์ workflow ใหม่นี้ด้วย (อย่าลืมดู requirements.txt) จากนั้นให้...
🄶 commit งานที่ทำ แล้ว push กลับไปที่ github ด้วย อย่าลืมรอดูผล Backend Tests
ถ้ารันได้เรียบร้อยน่าจะเห็นลักษณะนี้
