Home > Blog > tech

Software Testing คืออะไร? สอน Unit Test, Integration Test, E2E Test พร้อมตัวอย่างโค้ด 2026

software testing guide
Software Testing Guide 2026
2026-04-08 | tech | 3500 words

ในโลกของการพัฒนาซอฟต์แวร์ยุค 2026 การเขียนโค้ดที่ทำงานได้อย่างถูกต้องเพียงอย่างเดียวไม่เพียงพออีกต่อไป Software Testing หรือการทดสอบซอฟต์แวร์คือกระบวนการที่สำคัญอย่างยิ่งในการรับประกันคุณภาพของโปรแกรม ไม่ว่าจะเป็นแอปพลิเคชันขนาดเล็กหรือระบบขนาดใหญ่ระดับ Enterprise ทุกระบบจำเป็นต้องมีการทดสอบที่ครอบคลุมเพื่อให้มั่นใจว่าทุกฟังก์ชันทำงานได้ตามที่คาดหวัง ไม่มีบั๊กแฝง และสามารถรองรับการเปลี่ยนแปลงในอนาคตได้อย่างมั่นคง

บทความนี้จะพาคุณเรียนรู้ Software Testing ตั้งแต่พื้นฐานจนถึงระดับที่สามารถนำไปใช้จริงในโปรเจกต์ได้ ครอบคลุมทั้ง Unit Testing, Integration Testing, End-to-End Testing, TDD, BDD พร้อมตัวอย่างโค้ดจากหลายภาษาและเฟรมเวิร์ก

ทำไม Software Testing ถึงสำคัญ?

หลายคนอาจคิดว่าการเขียนเทสต์คือการเสียเวลา แต่ในความเป็นจริงแล้ว การลงทุนเวลาในการเขียนเทสต์ช่วยประหยัดเวลาและค่าใช้จ่ายมหาศาลในระยะยาว จากการศึกษาของ IBM พบว่าค่าใช้จ่ายในการแก้บั๊กที่พบในขั้นตอน Production สูงกว่าบั๊กที่พบในขั้นตอน Development ถึง 6 เท่า และสูงกว่าบั๊กที่พบในขั้นตอนการออกแบบถึง 15 เท่า

ประโยชน์หลักของ Software Testing มีดังนี้:

Testing Pyramid — ปิรามิดการทดสอบ

Testing Pyramid เป็นแนวคิดที่ Mike Cohn เสนอในหนังสือ "Succeeding with Agile" ซึ่งอธิบายสัดส่วนที่เหมาะสมของการทดสอบแต่ละประเภท โดยแบ่งเป็น 3 ชั้นหลัก:

ชั้นประเภทสัดส่วนความเร็วค่าใช้จ่าย
บนE2E Tests10%ช้าสูง
กลางIntegration Tests20%ปานกลางปานกลาง
ล่างUnit Tests70%เร็วมากต่ำ

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

Unit Testing — การทดสอบหน่วยย่อยที่สุด

Unit Test คือการทดสอบส่วนที่เล็กที่สุดของโค้ด เช่น ฟังก์ชันเดียว หรือเมธอดเดียวของ Class โดยแยกส่วนที่ทดสอบออกจาก Dependencies ภายนอกทั้งหมด เพื่อให้แน่ใจว่าแต่ละหน่วยทำงานถูกต้องด้วยตัวเอง

AAA Pattern (Arrange-Act-Assert)

รูปแบบที่นิยมที่สุดในการเขียน Unit Test คือ AAA Pattern ซึ่งแบ่งเทสต์แต่ละตัวออกเป็น 3 ส่วนชัดเจน ทำให้อ่านง่ายและบำรุงรักษาได้ง่าย:

# Python — Pytest ตัวอย่าง AAA Pattern
def test_calculate_discount():
    # Arrange — เตรียมข้อมูลทดสอบ
    price = 1000
    discount_percent = 20

    # Act — เรียกฟังก์ชันที่ต้องการทดสอบ
    result = calculate_discount(price, discount_percent)

    # Assert — ตรวจสอบผลลัพธ์
    assert result == 800
// JavaScript — Jest ตัวอย่าง AAA Pattern
describe('calculateDiscount', () => {
  test('should apply 20% discount correctly', () => {
    // Arrange
    const price = 1000;
    const discountPercent = 20;

    // Act
    const result = calculateDiscount(price, discountPercent);

    // Assert
    expect(result).toBe(800);
  });

  test('should return original price when discount is 0', () => {
    expect(calculateDiscount(500, 0)).toBe(500);
  });

  test('should throw error for negative discount', () => {
    expect(() => calculateDiscount(500, -10)).toThrow('Invalid discount');
  });
});
// Go — testing package
func TestCalculateDiscount(t *testing.T) {
    // Arrange
    price := 1000.0
    discountPercent := 20.0

    // Act
    result := CalculateDiscount(price, discountPercent)

    // Assert
    expected := 800.0
    if result != expected {
        t.Errorf("CalculateDiscount(%f, %f) = %f; want %f",
            price, discountPercent, result, expected)
    }
}

Mocking — จำลอง Dependencies

ในการเขียน Unit Test บ่อยครั้งที่ฟังก์ชันที่เราต้องการทดสอบมีการเรียกใช้ Dependencies ภายนอก เช่น Database, API, File System เราจำเป็นต้อง Mock (จำลอง) Dependencies เหล่านี้ เพื่อให้เทสต์ทำงานเร็วและไม่ขึ้นอยู่กับระบบภายนอก

# Python — unittest.mock
from unittest.mock import Mock, patch, MagicMock

class TestUserService:
    def test_get_user_by_id(self):
        # Arrange — Mock repository
        mock_repo = Mock()
        mock_repo.find_by_id.return_value = {
            "id": 1, "name": "สมชาย", "email": "somchai@example.com"
        }
        service = UserService(repository=mock_repo)

        # Act
        user = service.get_user(1)

        # Assert
        assert user["name"] == "สมชาย"
        mock_repo.find_by_id.assert_called_once_with(1)

    @patch('services.user_service.requests.get')
    def test_fetch_external_profile(self, mock_get):
        # Arrange
        mock_response = Mock()
        mock_response.json.return_value = {"avatar": "https://example.com/pic.jpg"}
        mock_response.status_code = 200
        mock_get.return_value = mock_response

        service = UserService()

        # Act
        profile = service.fetch_external_profile("user123")

        # Assert
        assert profile["avatar"] == "https://example.com/pic.jpg"
        mock_get.assert_called_once()
// JavaScript — Jest Mocking
// ไฟล์: userService.test.js
jest.mock('./database');
const db = require('./database');

describe('UserService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('should fetch user from database', async () => {
    // Arrange
    db.query.mockResolvedValue({ id: 1, name: 'สมหญิง' });

    // Act
    const user = await UserService.getUser(1);

    // Assert
    expect(user.name).toBe('สมหญิง');
    expect(db.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = ?', [1]);
  });

  test('should throw when user not found', async () => {
    db.query.mockResolvedValue(null);
    await expect(UserService.getUser(999)).rejects.toThrow('User not found');
  });
});
Mock เฉพาะสิ่งที่จำเป็น: อย่า Mock ทุกอย่าง Mock เฉพาะ Dependencies ภายนอก (Database, API, File I/O) ส่วน Logic ภายในควรทดสอบจริง ถ้าต้อง Mock มากเกินไป อาจเป็นสัญญาณว่าโค้ดมี Coupling สูงเกินไปและควร Refactor

Integration Testing — ทดสอบการทำงานร่วมกัน

Integration Test คือการทดสอบว่าหลายส่วนของระบบทำงานร่วมกันได้อย่างถูกต้อง ต่างจาก Unit Test ที่ทดสอบแต่ละหน่วยแยกกัน Integration Test จะทดสอบว่าเมื่อนำหน่วยต่างๆ มาต่อกันแล้ว ระบบทำงานได้ตามที่คาดหวัง ตัวอย่างเช่น การทดสอบว่า API Endpoint สามารถรับข้อมูล ประมวลผล และบันทึกลง Database ได้ถูกต้อง

ทดสอบ API ด้วย Supertest (Node.js)

// JavaScript — Supertest + Express
const request = require('supertest');
const app = require('../app');
const db = require('../database');

describe('POST /api/users', () => {
  beforeAll(async () => {
    await db.migrate();
  });

  afterEach(async () => {
    await db.query('DELETE FROM users');
  });

  afterAll(async () => {
    await db.close();
  });

  test('should create a new user and return 201', async () => {
    const newUser = {
      name: 'ทดสอบ',
      email: 'test@example.com',
      password: 'secure123'
    };

    const res = await request(app)
      .post('/api/users')
      .send(newUser)
      .expect(201);

    expect(res.body.name).toBe('ทดสอบ');
    expect(res.body.email).toBe('test@example.com');
    expect(res.body).not.toHaveProperty('password');

    // ตรวจสอบว่าบันทึกลง Database จริง
    const dbUser = await db.query('SELECT * FROM users WHERE email = ?', ['test@example.com']);
    expect(dbUser).toBeDefined();
  });

  test('should return 400 for invalid email', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'ทดสอบ', email: 'not-an-email', password: '123' })
      .expect(400);

    expect(res.body.error).toContain('email');
  });

  test('should return 409 for duplicate email', async () => {
    const user = { name: 'ทดสอบ', email: 'dup@example.com', password: 'pass123' };
    await request(app).post('/api/users').send(user);
    const res = await request(app).post('/api/users').send(user).expect(409);
    expect(res.body.error).toContain('already exists');
  });
});

ทดสอบ API ด้วย httpx (Python)

# Python — FastAPI + httpx
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.database import get_db, Base, engine

@pytest.fixture(autouse=True)
async def setup_db():
    Base.metadata.create_all(bind=engine)
    yield
    Base.metadata.drop_all(bind=engine)

@pytest.mark.asyncio
async def test_create_and_get_product():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        # สร้างสินค้า
        create_res = await client.post("/api/products", json={
            "name": "เมาส์ไร้สาย",
            "price": 599.0,
            "stock": 100
        })
        assert create_res.status_code == 201
        product_id = create_res.json()["id"]

        # ดึงสินค้า
        get_res = await client.get(f"/api/products/{product_id}")
        assert get_res.status_code == 200
        assert get_res.json()["name"] == "เมาส์ไร้สาย"
        assert get_res.json()["price"] == 599.0

Testcontainers — จำลอง Database จริง

Testcontainers เป็นเครื่องมือที่ช่วยให้สามารถสร้าง Container ของ Database จริง (เช่น PostgreSQL, MySQL, Redis) ขึ้นมาใช้ในเทสต์ได้ ทำให้เทสต์ใกล้เคียงกับ Production มากที่สุด

# Python — Testcontainers
from testcontainers.postgres import PostgresContainer
import psycopg2

def test_database_operations():
    with PostgresContainer("postgres:16") as postgres:
        # เชื่อมต่อ Database จริงใน Container
        conn = psycopg2.connect(
            host=postgres.get_container_host_ip(),
            port=postgres.get_exposed_port(5432),
            user=postgres.username,
            password=postgres.password,
            dbname=postgres.dbname
        )
        cursor = conn.cursor()

        # สร้างตาราง
        cursor.execute("""
            CREATE TABLE products (
                id SERIAL PRIMARY KEY,
                name TEXT NOT NULL,
                price DECIMAL(10,2) NOT NULL
            )
        """)
        conn.commit()

        # ทดสอบ Insert
        cursor.execute("INSERT INTO products (name, price) VALUES (%s, %s) RETURNING id",
                       ("คีย์บอร์ด", 1290.00))
        product_id = cursor.fetchone()[0]
        assert product_id is not None

        # ทดสอบ Select
        cursor.execute("SELECT name, price FROM products WHERE id = %s", (product_id,))
        row = cursor.fetchone()
        assert row[0] == "คีย์บอร์ด"
        assert float(row[1]) == 1290.00

        conn.close()
    # Container จะถูกลบอัตโนมัติ

E2E Testing — ทดสอบทั้งระบบจากมุมผู้ใช้

End-to-End (E2E) Test คือการทดสอบระบบทั้งหมดจากมุมมองของผู้ใช้จริง โดยจำลองการใช้งานเบราว์เซอร์ คลิกปุ่ม กรอกฟอร์ม และตรวจสอบว่าผลลัพธ์ที่แสดงบนหน้าจอถูกต้อง เป็นเทสต์ที่ให้ความมั่นใจสูงสุดว่าระบบทำงานได้จริง แต่ก็ช้าและเปราะบางที่สุดเช่นกัน

Playwright — E2E Testing Framework ยอดนิยม 2026

// JavaScript — Playwright
const { test, expect } = require('@playwright/test');

test.describe('ระบบสมัครสมาชิก', () => {
  test('ผู้ใช้สามารถสมัครสมาชิกและเข้าสู่ระบบได้', async ({ page }) => {
    // ไปหน้าสมัครสมาชิก
    await page.goto('http://localhost:3000/register');

    // กรอกฟอร์ม
    await page.fill('#name', 'สมชาย ใจดี');
    await page.fill('#email', 'somchai@test.com');
    await page.fill('#password', 'P@ssw0rd123');
    await page.fill('#confirm-password', 'P@ssw0rd123');

    // คลิกปุ่มสมัคร
    await page.click('button[type="submit"]');

    // ตรวจสอบว่าสมัครสำเร็จ
    await expect(page.locator('.success-message')).toContainText('สมัครสมาชิกสำเร็จ');

    // เข้าสู่ระบบ
    await page.goto('http://localhost:3000/login');
    await page.fill('#email', 'somchai@test.com');
    await page.fill('#password', 'P@ssw0rd123');
    await page.click('button[type="submit"]');

    // ตรวจสอบว่าเข้าสู่ระบบสำเร็จ
    await expect(page.locator('.user-name')).toContainText('สมชาย ใจดี');
    await expect(page).toHaveURL('http://localhost:3000/dashboard');
  });

  test('แสดงข้อผิดพลาดเมื่อ email ซ้ำ', async ({ page }) => {
    await page.goto('http://localhost:3000/register');
    await page.fill('#email', 'existing@test.com');
    await page.fill('#password', 'P@ssw0rd123');
    await page.click('button[type="submit"]');
    await expect(page.locator('.error-message')).toContainText('อีเมลนี้ถูกใช้แล้ว');
  });
});

Cypress — อีกทางเลือกยอดนิยม

// JavaScript — Cypress
describe('ระบบตะกร้าสินค้า', () => {
  beforeEach(() => {
    cy.visit('/shop');
  });

  it('เพิ่มสินค้าลงตะกร้าและชำระเงินได้', () => {
    // เพิ่มสินค้า
    cy.get('[data-testid="product-1"]').click();
    cy.get('[data-testid="add-to-cart"]').click();
    cy.get('.cart-count').should('contain', '1');

    // ไปที่ตะกร้า
    cy.get('[data-testid="cart-icon"]').click();
    cy.url().should('include', '/cart');
    cy.get('.cart-item').should('have.length', 1);

    // ชำระเงิน
    cy.get('[data-testid="checkout-btn"]').click();
    cy.get('#card-number').type('4242424242424242');
    cy.get('#expiry').type('12/28');
    cy.get('#cvv').type('123');
    cy.get('[data-testid="pay-btn"]').click();

    // ตรวจสอบผลลัพธ์
    cy.get('.order-confirmation').should('be.visible');
    cy.get('.order-status').should('contain', 'ชำระเงินสำเร็จ');
  });
});

TDD — Test-Driven Development

TDD (Test-Driven Development) คือวิธีการพัฒนาซอฟต์แวร์ที่เขียนเทสต์ก่อนเขียนโค้ดจริง วงจรของ TDD มี 3 ขั้นตอนที่เรียกว่า Red-Green-Refactor:

  1. Red — เขียนเทสต์ที่ Fail (เพราะยังไม่มีโค้ดจริง)
  2. Green — เขียนโค้ดน้อยที่สุดเพื่อให้เทสต์ผ่าน
  3. Refactor — ปรับปรุงโค้ดให้สะอาด โดยเทสต์ยังคงผ่าน
# Python — ตัวอย่าง TDD สร้าง Password Validator

# ขั้นตอน 1: RED — เขียนเทสต์ก่อน (จะ Fail)
def test_password_must_be_at_least_8_chars():
    assert validate_password("short") == False

def test_password_must_contain_uppercase():
    assert validate_password("alllowercase1!") == False

def test_password_must_contain_number():
    assert validate_password("NoNumberHere!") == False

def test_password_must_contain_special_char():
    assert validate_password("NoSpecial123") == False

def test_valid_password_returns_true():
    assert validate_password("Str0ng!Pass") == True

# ขั้นตอน 2: GREEN — เขียนโค้ดน้อยที่สุดให้ผ่าน
import re

def validate_password(password: str) -> bool:
    if len(password) < 8:
        return False
    if not re.search(r'[A-Z]', password):
        return False
    if not re.search(r'[0-9]', password):
        return False
    if not re.search(r'[!@#$%^&*]', password):
        return False
    return True

# ขั้นตอน 3: REFACTOR — ปรับปรุงให้สะอาด
def validate_password_v2(password: str) -> bool:
    rules = [
        (len(password) >= 8, "ต้องมีอย่างน้อย 8 ตัวอักษร"),
        (bool(re.search(r'[A-Z]', password)), "ต้องมีตัวพิมพ์ใหญ่"),
        (bool(re.search(r'[0-9]', password)), "ต้องมีตัวเลข"),
        (bool(re.search(r'[!@#$%^&*]', password)), "ต้องมีอักขระพิเศษ"),
    ]
    return all(passed for passed, _ in rules)

BDD — Behavior-Driven Development

BDD (Behavior-Driven Development) เป็นแนวทางที่ต่อยอดจาก TDD โดยเน้นการเขียนเทสต์ในรูปแบบภาษาธรรมชาติที่ทุกคนในทีม (รวมถึงคนที่ไม่ใช่โปรแกรมเมอร์) สามารถอ่านเข้าใจได้ ใช้รูปแบบ Given-When-Then:

# Gherkin — ไฟล์ .feature
Feature: ระบบเข้าสู่ระบบ
  ในฐานะผู้ใช้ที่มีบัญชี
  ฉันต้องการเข้าสู่ระบบ
  เพื่อเข้าถึงข้อมูลส่วนตัว

  Scenario: เข้าสู่ระบบด้วยข้อมูลที่ถูกต้อง
    Given ฉันอยู่ที่หน้าเข้าสู่ระบบ
    When ฉันกรอกอีเมล "somchai@example.com"
    And ฉันกรอกรหัสผ่าน "P@ssw0rd123"
    And ฉันกดปุ่มเข้าสู่ระบบ
    Then ฉันควรเห็นหน้า Dashboard
    And ฉันควรเห็นข้อความ "สวัสดี สมชาย"

  Scenario: เข้าสู่ระบบด้วยรหัสผ่านผิด
    Given ฉันอยู่ที่หน้าเข้าสู่ระบบ
    When ฉันกรอกอีเมล "somchai@example.com"
    And ฉันกรอกรหัสผ่าน "wrongpassword"
    And ฉันกดปุ่มเข้าสู่ระบบ
    Then ฉันควรเห็นข้อความผิดพลาด "อีเมลหรือรหัสผ่านไม่ถูกต้อง"

Testing Frameworks — เครื่องมือทดสอบยอดนิยม

ภาษา/RuntimeFrameworkจุดเด่น
JavaScript/TypeScriptJestAll-in-one, Snapshot testing, Mocking built-in
JavaScript/TypeScriptVitestเร็ว, รองรับ ESM, ใกล้ชิด Vite
PythonPytestง่าย, Fixture system, Plugin มากมาย
Gotesting (built-in)ไม่ต้องติดตั้งเพิ่ม, Table-driven tests
GotestifyAssertion helpers, Mock generation
JavaJUnit 5มาตรฐานอุตสาหกรรม, Parameterized tests
Rust#[test] (built-in)ไม่ต้องติดตั้งเพิ่ม, cargo test
E2EPlaywrightMulti-browser, Auto-wait, Codegen
E2ECypressDX ดี, Time-travel debugging

Go Table-Driven Tests

// Go — Table-driven tests (Best practice)
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -1, 5, 4},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

JUnit 5 Parameterized Tests

// Java — JUnit 5
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "100, 200, 300",
        "-1, 1, 0"
    })
    void testAdd(int a, int b, int expected) {
        Calculator calc = new Calculator();
        assertEquals(expected, calc.add(a, b));
    }

    @ParameterizedTest
    @CsvSource({
        "10, 0",
        "-5, 0"
    })
    void testDivideByZero(int a, int b) {
        Calculator calc = new Calculator();
        assertThrows(ArithmeticException.class, () -> calc.divide(a, b));
    }
}

Code Coverage — ความครอบคลุมของเทสต์

Code Coverage คือตัวชี้วัดที่บอกว่าเทสต์ของเราครอบคลุมโค้ดมากแค่ไหน โดยวัดจากจำนวนบรรทัด, Branch, หรือ Function ที่ถูกเรียกใช้ระหว่างรันเทสต์ เป็นเครื่องมือสำคัญในการประเมินคุณภาพของเทสต์

# Python — coverage.py
pip install coverage

# รันเทสต์พร้อมวัด Coverage
coverage run -m pytest
coverage report -m          # แสดงผลในเทอร์มินัล
coverage html               # สร้างรายงาน HTML

# ตัวอย่างผลลัพธ์:
# Name                    Stmts   Miss  Cover   Missing
# -------------------------------------------------------
# app/models.py              45      3    93%    22-24
# app/services.py            78     12    85%    45-56
# app/utils.py               30      0   100%
# -------------------------------------------------------
# TOTAL                     153     15    90%
// JavaScript — Istanbul/nyc + Jest
// package.json
{
  "scripts": {
    "test": "jest --coverage",
    "test:ci": "jest --coverage --coverageReporters='text' --coverageReporters='lcov'"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,ts}",
      "!src/**/*.test.{js,ts}"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}
Coverage 100% ไม่ได้หมายความว่าไม่มีบั๊ก: Coverage วัดแค่ว่าโค้ดบรรทัดไหนถูกรันในเทสต์ ไม่ได้วัดว่าเทสต์ครอบคลุมทุก Edge case ควรตั้งเป้า 80-90% และเน้นที่คุณภาพของ Assertion มากกว่าจำนวน Coverage

Snapshot Testing

Snapshot Testing เป็นเทคนิคที่บันทึกผลลัพธ์ของโค้ด (เช่น HTML, JSON, Component output) เป็น Snapshot ไฟล์ แล้วในรอบถัดไปจะเปรียบเทียบผลลัพธ์ใหม่กับ Snapshot เดิม ถ้าต่างกันเทสต์จะ Fail เหมาะสำหรับตรวจจับการเปลี่ยนแปลงที่ไม่ตั้งใจ

// Jest — Snapshot Testing
test('renders user profile correctly', () => {
  const component = render(<UserProfile name="สมชาย" role="admin" />);
  expect(component.toJSON()).toMatchSnapshot();
});

// ถ้าต้องการอัปเดต Snapshot เมื่อเปลี่ยนแปลงตั้งใจ:
// npx jest --updateSnapshot

Property-Based Testing

Property-Based Testing เป็นแนวทางที่แตกต่างจากเทสต์ทั่วไป แทนที่จะระบุ Input/Output ที่เจาะจง จะระบุ Properties (คุณสมบัติ) ที่ผลลัพธ์ต้องเป็นจริงเสมอ แล้วเฟรมเวิร์กจะสุ่ม Input ร้อยๆ พันๆ ตัวมาทดสอบ

# Python — Hypothesis
from hypothesis import given
import hypothesis.strategies as st

@given(st.lists(st.integers()))
def test_sort_preserves_length(lst):
    """เรียงลำดับแล้ว จำนวนสมาชิกต้องเท่าเดิม"""
    sorted_lst = sorted(lst)
    assert len(sorted_lst) == len(lst)

@given(st.lists(st.integers(), min_size=1))
def test_sort_first_element_is_minimum(lst):
    """สมาชิกตัวแรกของ List ที่เรียงแล้ว ต้องเป็นค่าน้อยสุด"""
    sorted_lst = sorted(lst)
    assert sorted_lst[0] == min(lst)

@given(st.text(), st.text())
def test_string_concat_length(a, b):
    """ความยาวของ String ที่ต่อกัน ต้องเท่ากับผลรวมความยาว"""
    assert len(a + b) == len(a) + len(b)

Mutation Testing

Mutation Testing เป็นเทคนิคขั้นสูงที่ใช้วัดคุณภาพของเทสต์ โดยเปลี่ยนแปลง (Mutate) โค้ดจริงเล็กน้อย เช่น เปลี่ยน + เป็น -, เปลี่ยน == เป็น != แล้วรันเทสต์ ถ้าเทสต์ยังผ่านแสดงว่าเทสต์ไม่ดีพอ (ไม่สามารถตรวจจับความผิดพลาดได้)

# Python — mutmut
pip install mutmut

# รัน Mutation testing
mutmut run --paths-to-mutate=app/

# ดูผลลัพธ์
mutmut results
# Killed: 45    — เทสต์ตรวจจับ Mutation ได้ (ดี)
# Survived: 5   — เทสต์ไม่ตรวจจับ (ต้องเพิ่มเทสต์)
# Score: 90%

การทดสอบใน CI/CD Pipeline

การเทสต์จะมีประสิทธิภาพสูงสุดเมื่อรันอัตโนมัติทุกครั้งที่มีการ Push โค้ดหรือสร้าง Pull Request ตัวอย่างการตั้งค่า CI/CD pipeline:

# GitHub Actions — .github/workflows/test.yml
name: Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:unit -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          file: ./coverage/lcov.info

  integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports: ['5432:5432']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:integration
        env:
          DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e

Testing Anti-Patterns — สิ่งที่ไม่ควรทำ

การเขียนเทสต์ที่ไม่ดีอาจส่งผลเสียมากกว่าการไม่มีเทสต์เลย เพราะให้ความมั่นใจปลอม (False confidence) และเสียเวลาในการบำรุงรักษา ต่อไปนี้คือ Anti-patterns ที่พบบ่อย:

Test Naming Conventions — การตั้งชื่อเทสต์ที่ดี

ชื่อเทสต์ที่ดีต้องบอกได้ทันทีว่าเทสต์อะไร ภายใต้เงื่อนไขอะไร และผลลัพธ์ที่คาดหวังคืออะไร:

# รูปแบบที่ 1: should_[ผลลัพธ์]_when_[เงื่อนไข]
def test_should_return_error_when_email_is_invalid():
    ...

def test_should_create_user_when_all_fields_valid():
    ...

# รูปแบบที่ 2: [method]_[scenario]_[expected]
def test_calculate_discount_with_zero_percent_returns_original_price():
    ...

def test_validate_password_with_short_input_returns_false():
    ...

# รูปแบบที่ 3: Given-When-Then ในชื่อ
def test_given_logged_in_user_when_access_profile_then_returns_200():
    ...

เมื่อไหร่ควรเขียนเทสต์?

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

กฎง่ายๆ: ถ้าคุณต้องเปิดเบราว์เซอร์หรือรันโปรแกรมเพื่อทดสอบฟังก์ชันทุกครั้งที่แก้ไข ให้เขียน Automated Test แทน จะช่วยประหยัดเวลาในระยะยาว

Pytest Fixtures — จัดการ Test Setup

# Python — Pytest Fixtures ขั้นสูง
import pytest
from app import create_app
from app.database import db as _db

@pytest.fixture(scope='session')
def app():
    """สร้าง Application สำหรับทั้ง Test session"""
    app = create_app('testing')
    return app

@pytest.fixture(scope='function')
def db(app):
    """สร้าง Database ใหม่ทุกเทสต์"""
    with app.app_context():
        _db.create_all()
        yield _db
        _db.session.rollback()
        _db.drop_all()

@pytest.fixture
def client(app):
    """สร้าง Test client"""
    return app.test_client()

@pytest.fixture
def sample_user(db):
    """สร้าง User ตัวอย่าง"""
    from app.models import User
    user = User(name="ทดสอบ", email="test@example.com")
    user.set_password("TestPass123!")
    db.session.add(user)
    db.session.commit()
    return user

# ใช้งาน Fixtures
def test_get_user_profile(client, sample_user):
    res = client.get(f'/api/users/{sample_user.id}')
    assert res.status_code == 200
    assert res.json['name'] == 'ทดสอบ'

def test_update_user(client, sample_user):
    res = client.put(f'/api/users/{sample_user.id}', json={"name": "ชื่อใหม่"})
    assert res.status_code == 200
    assert res.json['name'] == 'ชื่อใหม่'

สรุป — Best Practices ของ Software Testing

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

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


Back to Blog | iCafe Forex | SiamLanCard | Siam2R