FutureQuest

FutureQuest

  • Home
  • Courses
  • Blog
  • Contact

วัยรุ่นเทสดี : Part2 เข้าใจ Assertions และการจัดระเบียบเทสอย่างโปรฯ

Written by

Thitipong Suhuttaporn

in

Software Testing
August 9, 2025

กลับมาอีกครั้งกับซีรีส์ วัยรุ่นเทสดี หลังจากตอนที่แล้วเราได้รู้จักกับ pytest และเขียนเทสแรกกันไปแล้ว วันนี้เราจะมาลงลึกกันเรื่อง Assertions ที่ทรงพลังกว่าแค่ assert a == b และเทคนิคการจัดระเบียบเทสให้เป็นระบบเหมือนนักพัฒนามืออาชีพ!

คราวนี้เราจะเห็น pytest ในแง่มุมที่เจ๋งและใช้งานได้จริงมากขึ้น พร้อมทั้งเรียนรู้วิธีการเขียนเทสที่ maintainable และ scalable 🎯

Contents

Toggle
  • Assertions คืออะไร?
  • Assert ไม่ได้มีแค่ == เท่านั้นนะ!
    • 1. การเปรียบเทียบพื้นฐาน
    • 2. Membership
    • 3. ตรวจสอบชนิดของตัวแปร
    • 4. การตวจสอบ Exception
  • แก้ไข Project ให้มีการ raise exception
  • แก้ไข Tests ให้รองรับ Exception
  • การจัดระเบียบเทสแบบมืออาชีพ
    • 1. โครงสร้างไฟล์ที่ดี
    • 2. การใช้คลาสจัดกลุ่มเทส
    • 3. การใช้ pytest.mark.parametrize
    • 4. การใช้ Markers เพื่อจัดกลุ่มเทส
    • Config pytest ด้วยไฟล์ pytest.ini
    • รันเทสตาม Marker
  • source code

Assertions คืออะไร?

Assertions คือการตรวจสอบเงื่อนไขภายในโค้ดด้วยคำสั่ง assert โดยทั่วไปแล้วจะใช้ในระหว่างการทดสอบเพื่อยืนยันว่า ผลลัพธ์ที่ได้ตรงตามที่คาดหวังหรือไม่ ถ้าเงื่อนไขใน assert เป็น False โปรแกรมจะหยุดทำงานและแสดงข้อผิดพลาด

Assert ไม่ได้มีแค่ == เท่านั้นนะ!

หลายคนอาจคิดว่า assert ก็แค่เช็คว่าค่าเท่ากันหรือไม่ แต่จริงๆ แล้วเราสามารถใช้ assert ได้หลากหลายมากขึ้น มาดูกันว่า pytest รองรับอะไรบ้าง

1. การเปรียบเทียบพื้นฐาน

# test_assertions_basic.py
def test_equality_assertions():
    # เทสการเปรียบเทียบความเท่ากัน
    assert 2 + 2 == 4
    assert "hello" == "hello"
    assert [1, 2, 3] == [1, 2, 3]

def test_inequality_assertions():
    # เทสการเปรียบเทียบความไม่เท่ากัน
    assert 5 != 3
    assert 10 > 5
    assert 3 < 7
    assert 5 >= 5
    assert 5 <= 10

def test_boolean_assertions():
    # เทสค่า Boolean
    assert True
    assert not False
    assert bool([1, 2, 3])  # list ที่มีข้อมูลจะเป็น True
    assert not bool([])     # list ว่างจะเป็น False

2. Membership

def test_membership():
    # เทสการตรวจสอบสมาชิกภาพ"""
    fruits = ["apple", "banana", "orange"]
    
    # เช็คว่ามีสมาชิกหรือไม่
    assert "apple" in fruits
    assert "mango" not in fruits
    
    # เช็คใน string
    assert "test" in "pytest is the best"
    assert "java" not in "python is awesome"

def test_substring_checking():
    # เทสการเช็ค substring
    message = "Hello World"
    assert "Hello" in message
    assert "World" in message
    assert "Python" not in message

3. ตรวจสอบชนิดของตัวแปร

def test_type_checking():
    # เทสการตรวจสอบ type
    assert isinstance(42, int)
    assert isinstance("hello", str)
    assert isinstance([1, 2, 3], list)
    assert isinstance({"key": "value"}, dict)
    
def test_type_validation():
    # เทสการ validate type แบบเฉพาะเจาะจง
    # ฟังก์ชันที่ต้องการ return type เฉพาะ
    def get_user_age() -> int:
        return 25
    
    result = get_user_age()
    assert isinstance(result, int) # ตรวจสอบว่าตัวแปรเป็น int
    assert result > 0 # ตรวจสอบว่าตัวแปรมีค่ามากกว่า 0

4. การตวจสอบ Exception

เราสามารถใช้ pytest.raises เพื่อเช็คว่า exception ที่ต้องการให้ raise เมื่อมีความผิดปกติบางอย่างภายใน function ถูก raise ขึ้นมาหรือไม่

import pytest

def test_exception_with_message():
    """เทสการเช็ค Exception พร้อมการตรวจสอบ message"""
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide_numbers(10, 0)

def test_exception_type_only():
    """เทสการเช็ค Exception เฉพาะ type"""
    with pytest.raises(TypeError):
        divide_numbers("10", 5)
        
def test_exception_capture():
    """เทสการ capture exception เพื่อตรวจสอบรายละเอียดอื่นๆ ที่เกี่ยวข้อง"""
    with pytest.raises(ValueError) as exc_info:
        divide_numbers(10, 0)
    
    # ตรวจสอบ message ของ exception
    assert "Cannot divide by zero" in str(exc_info.value)

แก้ไข Project ให้มีการ raise exception

ขั้นตอนการ setup project จะอยู่ใน part1

แก้ไขไฟล์ src/calculator.py เพิ่มการตรวจสอบ input ในฟังก์ชันต่างๆ ของ calculator และ raise exception เมื่อ input ไม่ถูกต้อง

# src/calculator.py
def add(x,y):
    if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
        raise TypeError("Both input must be number")
    return x + y

def subtract(x,y):
    if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
        raise TypeError("Both input must be number")
    return x - y

def multiply(x,y):
    if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
        raise TypeError("Both input must be number")
    return x * y

def divide(num,den):
    if den == 0:
        raise ZeroDivisionError("Denominator cannot be zero")
    if not isinstance(num, (int, float)) or not isinstance(den, (int, float)):
        raise TypeError("Both input must be number")
    return num / den

แก้ไข Tests ให้รองรับ Exception

เราจะแก้ไข file test/test_calculator.py โดยจะเพิ่ม test case

# tests/test_calculator.py

from src.calculator import add, subtract, multiply, divide
import pytest

def test_add_positive_numbers():
    assert add(1, 2) == 3
    assert add(10, 5) == 15
    assert add(0, 0) == 0

def test_add_decimal_numbers():
    assert add(1.1, 2.5) == 3.6
    assert add(10.1, 5.95) == 16.05
    assert add(0.0, 0.0) == 0.0
    assert add(1.25, 2.13) == 3.38 
    
def test_add_negative_numbers():
    assert add(-1, -1) == -2
    assert add(-10, -5) == -15
    assert add(-500, -5) == -505
    
def test_add_mixed_numbers():
    assert add(-10, 3) == -7
    assert add(5, -3) == 2
    
def test_add_non_numeric_input():
    with pytest.raises(TypeError, match="Both input must be number"):
        add("1", 2)
    with pytest.raises(TypeError, match="Both input must be number"):
        add(1, "2")
    with pytest.raises(TypeError, match="Both input must be number"):
        add("1", "2")
    
def test_subtract_positive_numbers():
    assert subtract(5, 2) == 3
    assert subtract(10, 5) == 5
    assert subtract(7, 25) == -18

def test_subtract_negative_numbers():
    assert subtract(-5, -2) == -3
    assert subtract(-10, -5) == -5
    assert subtract(-7, -25) == 18
    
def test_subtract_mixed_numbers():
    assert subtract(-5, 2) == -7
    assert subtract(5, -2) == 7
    
def test_substract_decimal_numbers():
    assert subtract(5.5, 2.2) == 3.3
    assert subtract(10.1, 5.95) == 4.15
    assert subtract(0.0, 0.0) == 0.0
    
def test_subtract_non_numeric_input():
    with pytest.raises(TypeError, match="Both input must be number"):
        subtract("5", 2)
    with pytest.raises(TypeError, match="Both input must be number"):
        subtract(5, "2")
    with pytest.raises(TypeError, match="Both input must be number"):
        subtract("5", "2")
    
def test_multiply_positive_numbers():
    assert multiply(2, 3) == 6
    assert multiply(5, 4) == 20
    assert multiply(0, 5) == 0

def test_multiply_negative_numbers():
    assert multiply(-2, -3) == 6
    assert multiply(-5, -4) == 20
    assert multiply(-2, -4) == 8

def test_multiply_mixed_numbers():
    assert multiply(-2, 3) == -6
    assert multiply(2, -3) == -6
    assert multiply(0, 5) == 0
    
def test_multiply_decimal_numbers():
    assert multiply(2.5, 4) == 10
    assert multiply(3.1, 2.0) == 6.2
    assert multiply(0.0, 5) == 0.0
    
def test_multiply_non_numeric_input():
    with pytest.raises(TypeError, match="Both input must be number"):
        multiply("2", 3)
    with pytest.raises(TypeError, match="Both input must be number"):
        multiply(2, "3")
    with pytest.raises(TypeError, match="Both input must be number"):
        multiply("2", "3")
    
def test_divide_positive_numbers():
    assert divide(6, 2) == 3
    assert divide(5, 2) == 2.5
    
def test_divide_negative_numbers():
    assert divide(-6, -2) == 3
    assert divide(-5, -2) == 2.5
    
def test_divide_mixed_numbers():
    assert divide(-6, 2) == -3
    assert divide(6, -2) == -3
    assert divide(0, 5) == 0
    
def test_divide_decimal_numbers():
    assert divide(5.5, 2.2) == 2.5
    assert divide(10.1, 5.05) == 2.0
    assert divide(0.0, 1.0) == 0.0
    
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError, match="Denominator cannot be zero"):
        divide(5, 0)
    with pytest.raises(ZeroDivisionError, match="Denominator cannot be zero"):
        divide(0, 0)
        
def test_divide_non_numeric_input():
    with pytest.raises(TypeError, match="Both input must be number"):
        divide("6", 2)
    with pytest.raises(TypeError, match="Both input must be number"):
        divide(6, "2")
    with pytest.raises(TypeError, match="Both input must be number"):
        divide("6", "2")

ลองรันเทสด้วยคำสั่ง: pytest -v

จะเห็นว่ามีเทสที่พังด้วย ชิบหายแล้ว! 😱 มันเกิดจากอะไรลองมาดูกัน

ทำไม 10.1 – 5.95 ถึงไม่เท่ากับ 4.15?

เพราะการเก็บข้อมูลเลขทศนิยมในคอมพิวเตอร์มีความแม่นยำไม่สมบูรณ์ เราจึงต้องใช้การเช็คความใกล้เคียงแทนการเทียบเท่าตรงๆ

ปัญหานี้เรียกว่า floating-point precision issue ส่วนใครอยากเข้าใจเรื่องนี้ให้ลึกซึ้งขึ้น แนะนำให้ไปอ่านที่บทความนี้ได้เลย “ทำไมคอมพิวเตอร์บวกทศนิยมผิด“

เราจะแก้ปัญหานี้โดยการใช้ pytest.approx ซึ่งเป็นฟังก์ชันที่ช่วยให้เราสามารถเช็คความใกล้เคียงของค่าทศนิยมได้

def test_substract_decimal_numbers():
    assert subtract(5.5, 2.2) == pytest.approx(3.3)
    assert subtract(10.1, 5.95) == pytest.approx(4.15)
    assert subtract(0.0, 0.0) == 0.0

หลังจากแก้ไขแล้วลองรันเทสใหม่อีกครั้ง จะเห็นว่าเทสทั้งหมดผ่านเรียบร้อยแล้ว! 🎉

การจัดระเบียบเทสแบบมืออาชีพ

จากตัวอย่างที่ผ่านมาเราจะเริ่มเห็นว่า เทสที่เราเขียนมันชักจะเริ่มยาวและเยอะแล้วหล่ะสิ แล้วจะมีวิธีการจัดระเบียบโค้ดพวกนี้อย่างไร

1. โครงสร้างไฟล์ที่ดี

wai_roon_test_dee/
├── calculator/
│   ├── basic_operations.py
│   └── advanced_operations.py
├── tests/
│   ├── test_basic_operations.py
│   ├── test_advanced_operations.py
│   └── conftest.py              # ไฟล์สำหรับ shared fixtures
├── requirements.txt
└── pytest.ini                  # ไฟล์ config ของ pytest

สำหรับโปรเจคขนาดใหญ่ การจัดระเบียบโครงสร้างไฟล์ให้เหมาะสมจะช่วยให้การดูแลรักษาโค้ดทำได้ง่ายขึ้น ตอนนี้โปรเจคเรายังไม่ถือว่าใหญ่ อาจจะไม่เห็นความจำเป็นในการแยกไฟล์เทส แต่เมื่อโปรเจคเริ่มโตขึ้น การแยกไฟล์และโฟลเดอร์จะช่วยให้เราจัดการได้ง่ายขึ้น

2. การใช้คลาสจัดกลุ่มเทส

เราสามารถสร้างคลาส สำหรับแต่ละกลุ่มของเทส TestAddition, TestSubtraction, TestMultiplication, และ TestDivision เพื่อให้โค้ดดูเป็นระเบียบและเข้าใจง่ายขึ้น

# tests/test_calculator.py
from src.calculator import add, subtract, multiply, divide
import pytest

class TestAddition:
    """Test cases for addition function"""
    def test_positive_numbers(self):
        assert add(1, 2) == 3
        assert add(10, 5) == 15
        assert add(0, 0) == 0

    def test_decimal_numbers(self):
        assert add(1.1, 2.5) == pytest.approx(3.6)
        assert add(10.1, 5.95) == pytest.approx(16.05)
        assert add(0.0, 0.0) == pytest.approx(0.0)
        assert add(1.25, 2.13) == pytest.approx(3.38)

    def test_negative_numbers(self):
        assert add(-1, -1) == -2
        assert add(-10, -5) == -15
        assert add(-500, -5) == -505

    def test_mixed_numbers(self):
        assert add(-10, 3) == -7
        assert add(5, -3) == 2

    def test_non_numeric_input(self):
        with pytest.raises(TypeError, match="Both input must be number"):
            add("1", 2)
        with pytest.raises(TypeError, match="Both input must be number"):
            add(1, "2")
        with pytest.raises(TypeError, match="Both input must be number"):
            add("1", "2")
    
class TestSubtraction:
    """Test cases for subtraction function"""
    
    def test_positive_numbers(self):
        assert subtract(5, 2) == 3
        assert subtract(10, 5) == 5
        assert subtract(7, 25) == -18

    def test_negative_numbers(self):
        assert subtract(-5, -2) == -3
        assert subtract(-10, -5) == -5
        assert subtract(-7, -25) == 18
        
    def test_mixed_numbers(self):
        assert subtract(-5, 2) == -7
        assert subtract(5, -2) == 7
        
    def test_decimal_numbers(self):
        assert subtract(5.5, 2.2) == pytest.approx(3.3)
        assert subtract(10.1, 5.95) == pytest.approx(4.15)
        assert subtract(0.0, 0.0) == 0.0

    def test_non_numeric_input(self):
        with pytest.raises(TypeError, match="Both input must be number"):
            subtract("5", 2)
        with pytest.raises(TypeError, match="Both input must be number"):
            subtract(5, "2")
        with pytest.raises(TypeError, match="Both input must be number"):
            subtract("5", "2")
            
class TestMultiplication:
    """Test cases for multiplication function"""
    def test_positive_numbers(self):
        assert multiply(2, 3) == 6
        assert multiply(5, 4) == 20
        assert multiply(0, 5) == 0

    def test_negative_numbers(self):
        assert multiply(-2, -3) == 6
        assert multiply(-5, -4) == 20
        assert multiply(-2, -4) == 8

    def test_mixed_numbers(self):
        assert multiply(-2, 3) == -6
        assert multiply(2, -3) == -6
        assert multiply(0, 5) == 0

    def test_decimal_numbers(self):
        assert multiply(2.5, 4) == pytest.approx(10.0)
        assert multiply(3.1, 2.0) == pytest.approx(6.2)
        assert multiply(0.0, 5) == 0.0

    def test_non_numeric_input(self):
        with pytest.raises(TypeError, match="Both input must be number"):
            multiply("2", 3)
        with pytest.raises(TypeError, match="Both input must be number"):
            multiply(2, "3")
        with pytest.raises(TypeError, match="Both input must be number"):
            multiply("2", "3")

class TestDivision:
    """Test cases for division function"""
    
    def test_positive_numbers(self):
        assert divide(6, 2) == 3
        assert divide(5, 2) == 2.5
        
    def test_negative_numbers(self):
        assert divide(-6, -2) == 3
        assert divide(-5, -2) == 2.5
        
    def test_mixed_numbers(self):
        assert divide(-6, 2) == -3
        assert divide(6, -2) == -3
        assert divide(0, 5) == 0
        
    def test_decimal_numbers(self):
        assert divide(5.5, 2.2) == pytest.approx(2.5)
        assert divide(10.1, 5.05) == pytest.approx(2.0)
        assert divide(0.0, 1.0) == pytest.approx(0.0)

    def test_by_zero(self):
        with pytest.raises(ZeroDivisionError, match="Denominator cannot be zero"):
            divide(5, 0)
        with pytest.raises(ZeroDivisionError, match="Denominator cannot be zero"):
            divide(0, 0)
            
    def test_non_numeric_input(self):
        with pytest.raises(TypeError, match="Both input must be number"):
            divide("6", 2)
        with pytest.raises(TypeError, match="Both input must be number"):
            divide(6, "2")
        with pytest.raises(TypeError, match="Both input must be number"):
            divide("6", "2")

ดูผลลัพธ์จาก test explorer เราจะเห็นว่าเทสถูกแบ่งออกเป็นกลุ่มๆ ตามฟังก์ชันที่ทดสอบ

3. การใช้ pytest.mark.parametrize

ลองสังเกตุดูจะเห็นว่ามีการเรียกใช้ assert ซ้ำๆกันอยู่ เรามาลองใช้ pytest.mark.parametrize เพื่อทำให้โค้ดดูเรียบร้อยขึ้นกัน

แก้ไขไฟล์ test/test_calculator.py

# tests/test_calculator.py
from src.calculator import add, subtract, multiply, divide
import pytest

class TestAddition:
    """Test cases for addition function"""
    @pytest.mark.parametrize("input1, input2, expected", [
        (1, 2, 3),      # Test case 1
        (10, 5, 15),    # Test case 2
        (0, 0, 0)       # Test case 3
    ])
    def test_positive_numbers(self, input1, input2, expected):
        assert add(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (1.1, 2.5, 3.6),
        (10.1, 5.95, 16.05),
        (0.0, 0.0, 0.0),
        (1.25, 2.13, 3.38)
    ])
    def test_decimal_numbers(self, input1, input2, expected):
        assert add(input1, input2) == pytest.approx(expected)

    @pytest.mark.parametrize("input1, input2, expected", [
        (-1, -1, -2),
        (-10, -5, -15),
        (-500, -5, -505)
    ])
    def test_negative_numbers(self, input1, input2, expected):
        assert add(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [(-10, 3, -7),(5, -3, 2)])
    def test_mixed_numbers(self, input1, input2, expected):
        assert add(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2", [("1", 2),(1, "2"),("1", "2")])
    def test_non_numeric_input(self, input1, input2):
        with pytest.raises(TypeError, match="Both input must be number"):
            add(input1, input2)
    
class TestSubtraction:
    """Test cases for subtraction function"""
    
    @pytest.mark.parametrize("input1, input2, expected", [
        (5, 2, 3),
        (10, 5, 5),
        (7, 25, -18)
    ])
    def test_positive_numbers(self, input1, input2, expected):
        assert subtract(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (-5, -2, -3),
        (-10, -5, -5),
        (-7, -25, 18)
    ])
    def test_negative_numbers(self, input1, input2, expected):
        assert subtract(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (-5, 2, -7),
        (5, -2, 7),
        (0, 5, -5)
    ])
    def test_mixed_numbers(self, input1, input2, expected):
        assert subtract(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (5.5, 2.2, 3.3),
        (10.1, 5.95, 4.15),
        (0.0, 0.0, 0.0),
        (1.25, 2.13, -0.88)
    ])
    def test_decimal_numbers(self, input1, input2, expected):
        assert subtract(input1, input2) == pytest.approx(expected)

    @pytest.mark.parametrize("input1, input2", [
        ("5", 2),
        (5, "2"),
        ("5", "2")
    ])
    def test_non_numeric_input(self, input1, input2):
        with pytest.raises(TypeError, match="Both input must be number"):
            subtract(input1, input2)
            
class TestMultiplication:
    """Test cases for multiplication function"""

    @pytest.mark.parametrize("input1, input2, expected", [
        (2, 3, 6),
        (5, 4, 20),
        (0, 5, 0)
    ])
    def test_positive_numbers(self, input1, input2, expected):
        assert multiply(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (-2, -3, 6),
        (-5, -4, 20),
        (-2, -4, 8)
    ])
    def test_negative_numbers(self, input1, input2, expected):
        assert multiply(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (-2, 3, -6),
        (2, -3, -6),
        (0, 5, 0)
    ])
    def test_mixed_numbers(self, input1, input2, expected):
        assert multiply(input1, input2) == expected

    @pytest.mark.parametrize("input1, input2, expected", [
        (2.5, 4, 10.0),
        (3.1, 2.0, 6.2),
        (0.0, 5, 0.0)
    ])
    def test_decimal_numbers(self, input1, input2, expected):
        assert multiply(input1, input2) == pytest.approx(expected)

    @pytest.mark.parametrize("input1, input2", [
        ("2", 3),
        (2, "3"),
        ("2", "3")
    ])
    def test_non_numeric_input(self, input1, input2):
        with pytest.raises(TypeError, match="Both input must be number"):
            multiply(input1, input2)

class TestDivision:
    """Test cases for division function"""
    
    @pytest.mark.parametrize("num, den, expected", [(6, 2, 3),(5, 2, 2.5),(0, 5, 0)])
    def test_positive_numbers(self, num, den, expected):
        assert divide(num, den) == expected

    @pytest.mark.parametrize("num, den, expected", [(-6, -2, 3),(-5, -2, 2.5)])
    def test_negative_numbers(self, num, den, expected):
        assert divide(num, den) == expected

    @pytest.mark.parametrize("num, den, expected", [(-6, 2, -3), (6, -2, -3), (0, -5, 0)])
    def test_mixed_numbers(self, num, den, expected):
        assert divide(num, den) == expected

    @pytest.mark.parametrize("num, den, expected", [(5.5, 2.2, 2.5), (10.1, 5.05, 2.0), (0.0, 1.0, 0.0)])
    def test_decimal_numbers(self, num, den, expected):
        assert divide(num, den) == pytest.approx(expected)

    @pytest.mark.parametrize("num", [-1, 0, 5])
    def test_by_zero(self, num):
        with pytest.raises(ZeroDivisionError, match="Denominator cannot be zero"):
            divide(num, 0)

    @pytest.mark.parametrize("num, den", [("6", 2), (6, "2"), ("6", "2")])
    def test_non_numeric_input(self, num, den):
        with pytest.raises(TypeError, match="Both input must be number"):
            divide(num, den)

pytest.mark.parametrize จะทำงานในระดับ test function ซึ่งช่วยให้เราสามารถกำหนดชุดข้อมูลที่ต้องใช้สำหรับการทดสอบได้หลายชุด ทำให้การทดสอบครอบคลุมกรณีต่างๆ ได้มากขึ้น โดยที่ pytest จะสร้าง test cases แยกกันสำหรับแต่ละชุดข้อมูล

4. การใช้ Markers เพื่อจัดกลุ่มเทส

Markers ช่วยให้เราจัดกลุ่มเทสและรันเฉพาะกลุ่มที่ต้องการได้
เราจะใช้ markers เพื่อจัดกลุ่มเทสตามประเภท positive_numbers, negative_numbers, mixed_numbers, decimal_numbers, และ non_numeric_input

class TestAddition:
    """Test cases for addition function"""
    
    @pytest.mark.positive_numbers
    @pytest.mark.parametrize("input1, input2, expected", [(1, 2, 3),(10, 5, 15),(0, 0, 0)])
    def test_positive_numbers(self, input1, input2, expected):
        assert add(input1, input2) == expected

    @pytest.mark.decimal_numbers
    @pytest.mark.parametrize("input1, input2, expected", [
        (1.1, 2.5, 3.6),
        (10.1, 5.95, 16.05),
        (0.0, 0.0, 0.0),
        (1.25, 2.13, 3.38)
    ])
    def test_decimal_numbers(self, input1, input2, expected):
        assert add(input1, input2) == pytest.approx(expected)

    @pytest.mark.negative_numbers
    @pytest.mark.parametrize("input1, input2, expected", [
        (-1, -1, -2),
        (-10, -5, -15),
        (-500, -5, -505)
    ])
    def test_negative_numbers(self, input1, input2, expected):
        assert add(input1, input2) == expected

    @pytest.mark.mixed_numbers
    @pytest.mark.parametrize("input1, input2, expected", [(-10, 3, -7),(5, -3, 2)])
    def test_mixed_numbers(self, input1, input2, expected):
        assert add(input1, input2) == expected

    @pytest.mark.non_numeric_input
    @pytest.mark.parametrize("input1, input2", [("1", 2),(1, "2"),("1", "2")])
    def test_non_numeric_input(self, input1, input2):
        with pytest.raises(TypeError, match="Both input must be number"):
            add(input1, input2)

สำหรับเทสอื่นๆ ก็ทำในลักษณะเดียวกัน โดยเพิ่ม markers ตามกลุ่มที่ต้องการ

Config pytest ด้วยไฟล์ pytest.ini

สร้างไฟล์ pytest.ini เพื่อกำหนดค่าต่างๆ:

[pytest]
markers =
    positive_numbers: Tests with positive numbers
    negative_numbers: Tests with negative numbers
    mixed_numbers: Tests with mixed positive and negative numbers
    decimal_numbers: Tests with decimal numbers
    non_numeric_input: Tests for non-numeric input
addopts = -v

รันเทสตาม Marker

# รันเฉพาะ เทสที่เป็น positive_numbers
pytest -m positive_numbers

# รันเฉพาะเทส ที่เป็น decimal_numbers
pytest -m decimal_numbers

# รันทุกเทสยกเว้นที่ non_numeric_input
pytest -m "not non_numeric_input"

# รันเทสที่เป็น positive_numbers หรือ negative_numbers
pytest -m "positive_numbers or negative_numbers"

# จะไม่มีเทสที่ถูกรันเลย เพราะไม่มีเทสไหนที่ระบุ marker positive_numbers และ negative_numbers พร้อมกัน
pytest -m "positive_numbers and negative_numbers"

ทดลองรันเฉพาะ เทสที่เป็น positive_numbers

เทสที่กำหนด marker positive_numbers ของทุกคลาสจะถูกรัน

source code

link

ยอดเยี่ยม! ตอนนี้เราได้เรียนรู้:

  • Assertions ที่หลากหลาย: ไม่ใช่แค่ == แต่มี in, isinstance, raises และอื่นๆ
  • การจัดระเบียบเทส: ใช้คลาส, parametrize, และ markers เพื่อทำให้โค้ดดูเรียบร้อยและเข้าใจง่าย
  • Configuration: ใช้ pytest.ini เพื่อตั้งค่าต่างๆ ของ pytest สำหรับโปรเจคของเราเช่น กำหนด markers

ในตอนหน้า เราจะไปลึกกับ Fixtures ที่จะทำให้เทสของเราทรงพลังและจัดการกับ dependencies ซับซ้อนได้อย่างมืออาชีพ!

รอติดตาม Part 3:Fixtures ตัวช่วยสุดล้ำ เตรียมพร้อม Test ได้ดั่งใจ! กันได้เลย! 🚀

pytest pytest-tutorial-series python Software Development software testing tutorial วัยรุ่นเทสดี

Subscribe to our newsletter

for more insights and exclusive content delivered straight to your inbox

←วัยรุ่นเทสดี : Part1 -ทำไม Developer ต้องทำ Software Testing

Comments

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

FutureQuest

Learn for our brighter future

Main Menu

Home

Courses

Blog

About Us

Contact Us

Resources

Privacy Policy

Term of Services

© 2025 futurequest.academy. All Rights Reserved.