ไทย

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

ฟังก์ชันจำลอง (Mock Functions): คู่มือฉบับสมบูรณ์สำหรับนักพัฒนา

ในโลกของการพัฒนาซอฟต์แวร์ การเขียนโค้ดที่แข็งแกร่งและเชื่อถือได้ถือเป็นสิ่งสำคัญอย่างยิ่ง การทดสอบอย่างละเอียดเป็นสิ่งสำคัญเพื่อให้บรรลุเป้าหมายนี้ โดยเฉพาะอย่างยิ่ง Unit testing ที่มุ่งเน้นการทดสอบส่วนประกอบหรือฟังก์ชันแต่ละส่วนแบบแยกเดี่ยว อย่างไรก็ตาม แอปพลิเคชันในโลกแห่งความเป็นจริงมักมีการพึ่งพาส่วนประกอบอื่นที่ซับซ้อน ทำให้การทดสอบแต่ละหน่วยแบบแยกเดี่ยวโดยสมบูรณ์เป็นเรื่องท้าทาย นี่คือจุดที่ฟังก์ชันจำลอง (mock functions) เข้ามามีบทบาท

ฟังก์ชันจำลอง (Mock Functions) คืออะไร?

ฟังก์ชันจำลองคือเวอร์ชันจำลองของฟังก์ชันจริงที่คุณสามารถใช้ในการทดสอบของคุณได้ แทนที่จะดำเนินการตามตรรกะของฟังก์ชันจริง ฟังก์ชันจำลองช่วยให้คุณสามารถควบคุมพฤติกรรมของมัน สังเกตวิธีการเรียกใช้งาน และกำหนดค่าที่จะส่งคืนได้ ฟังก์ชันเหล่านี้เป็นประเภทหนึ่งของ test double

ลองนึกภาพตามนี้: สมมติว่าคุณกำลังทดสอบเครื่องยนต์ของรถยนต์ (หน่วยที่กำลังทดสอบ) เครื่องยนต์ต้องอาศัยส่วนประกอบอื่นๆ เช่น ระบบหัวฉีดน้ำมันและระบบระบายความร้อน แทนที่จะใช้ระบบหัวฉีดและระบบระบายความร้อนจริงระหว่างการทดสอบเครื่องยนต์ คุณสามารถใช้ระบบจำลองที่เลียนแบบพฤติกรรมของมันได้ วิธีนี้ช่วยให้คุณสามารถแยกการทดสอบเครื่องยนต์ออกมาและมุ่งเน้นไปที่ประสิทธิภาพของมันโดยเฉพาะ

ฟังก์ชันจำลองเป็นเครื่องมือที่ทรงพลังสำหรับ:

ควรใช้ฟังก์ชันจำลองเมื่อใด

Mocks เป็นเครื่องมือที่มีประโยชน์ที่สุดในสถานการณ์เหล่านี้:

1. การแยกส่วนประกอบที่มีการพึ่งพาภายนอก

เมื่อหน่วยที่กำลังทดสอบของคุณต้องพึ่งพาบริการภายนอก ฐานข้อมูล API หรือส่วนประกอบอื่นๆ การใช้ส่วนประกอบจริงระหว่างการทดสอบอาจทำให้เกิดปัญหาหลายประการ:

ตัวอย่าง: ลองนึกภาพว่าคุณกำลังทดสอบฟังก์ชันที่ดึงข้อมูลผู้ใช้จาก API ระยะไกล แทนที่จะทำการเรียก API จริงระหว่างการทดสอบ คุณสามารถใช้ฟังก์ชันจำลองเพื่อจำลองการตอบสนองของ API ได้ ซึ่งช่วยให้คุณทดสอบตรรกะของฟังก์ชันโดยไม่ต้องพึ่งพาความพร้อมใช้งานหรือประสิทธิภาพของ API ภายนอก สิ่งนี้สำคัญอย่างยิ่งเมื่อ API มีการจำกัดอัตราการเรียก (rate limits) หรือมีค่าใช้จ่ายที่เกี่ยวข้องกับการร้องขอแต่ละครั้ง

2. การทดสอบปฏิสัมพันธ์ที่ซับซ้อน

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

ตัวอย่าง: พิจารณาฟังก์ชันที่ประมวลผลธุรกรรมการชำระเงิน ฟังก์ชันนี้อาจมีปฏิสัมพันธ์กับเกตเวย์การชำระเงิน ฐานข้อมูล และบริการแจ้งเตือน การใช้ฟังก์ชันจำลองทำให้คุณสามารถตรวจสอบได้ว่าฟังก์ชันเรียกใช้เกตเวย์การชำระเงินด้วยรายละเอียดธุรกรรมที่ถูกต้อง อัปเดตฐานข้อมูลด้วยสถานะธุรกรรม และส่งการแจ้งเตือนไปยังผู้ใช้

3. การจำลองสภาวะข้อผิดพลาด

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

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

4. การทดสอบโค้ดแบบอะซิงโครนัส (Asynchronous Code)

โค้ดแบบอะซิงโครนัส เช่น โค้ดที่ใช้ callbacks, promises หรือ async/await อาจเป็นเรื่องท้าทายในการทดสอบ ฟังก์ชันจำลองสามารถช่วยคุณควบคุมเวลาและพฤติกรรมของการดำเนินการแบบอะซิงโครนัสได้

ตัวอย่าง: ลองนึกภาพว่าคุณกำลังทดสอบฟังก์ชันที่ดึงข้อมูลจากเซิร์ฟเวอร์โดยใช้การร้องขอแบบอะซิงโครนัส คุณสามารถใช้ฟังก์ชันจำลองเพื่อจำลองการตอบสนองของเซิร์ฟเวอร์และควบคุมเวลาที่การตอบสนองจะถูกส่งคืนได้ ซึ่งช่วยให้คุณทดสอบว่าฟังก์ชันจัดการกับสถานการณ์การตอบสนองที่แตกต่างกันและหมดเวลา (timeouts) อย่างไร

5. การป้องกันผลข้างเคียงที่ไม่พึงประสงค์

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

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

วิธีใช้ฟังก์ชันจำลอง

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

  1. ระบุการพึ่งพา (Identify Dependencies): กำหนดว่าการพึ่งพาส่วนประกอบภายนอกใดที่คุณต้องการจำลอง
  2. สร้างอ็อบเจกต์จำลอง (Create Mock Objects): สร้างอ็อบเจกต์หรือฟังก์ชันจำลองเพื่อแทนที่การพึ่งพาส่วนประกอบจริง อ็อบเจกต์จำลองเหล่านี้มักจะมีคุณสมบัติเช่น `called`, `returnValue` และ `callArguments`
  3. กำหนดพฤติกรรมของ Mock (Configure Mock Behavior): กำหนดพฤติกรรมของฟังก์ชันจำลอง เช่น ค่าที่จะส่งคืน สภาวะข้อผิดพลาด และจำนวนครั้งที่เรียก
  4. ฉีด Mock เข้าไป (Inject Mocks): แทนที่การพึ่งพาส่วนประกอบจริงด้วยอ็อบเจกต์จำลองในหน่วยที่กำลังทดสอบของคุณ ซึ่งมักจะทำโดยใช้ Dependency Injection
  5. ดำเนินการทดสอบ (Execute Test): รันการทดสอบของคุณและสังเกตว่าหน่วยที่กำลังทดสอบมีปฏิสัมพันธ์กับฟังก์ชันจำลองอย่างไร
  6. ตรวจสอบปฏิสัมพันธ์ (Verify Interactions): ตรวจสอบว่าฟังก์ชันจำลองถูกเรียกด้วยอาร์กิวเมนต์ที่คาดหวัง ค่าคืนกลับ และจำนวนครั้งที่ถูกต้อง
  7. คืนค่าฟังก์ชันดั้งเดิม (Restore Original Functionality): หลังจากการทดสอบ ให้คืนค่าฟังก์ชันดั้งเดิมโดยการลบอ็อบเจกต์จำลองและกลับไปใช้การพึ่งพาส่วนประกอบจริง ซึ่งจะช่วยหลีกเลี่ยงผลข้างเคียงต่อการทดสอบอื่นๆ

ตัวอย่างฟังก์ชันจำลองในภาษาต่างๆ

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

JavaScript กับ Jest

Jest เป็นเฟรมเวิร์กการทดสอบ JavaScript ที่ได้รับความนิยมซึ่งมีการรองรับฟังก์ชันจำลองในตัว

// ฟังก์ชันที่จะทดสอบ
function fetchData(callback) {
  setTimeout(() => {
    callback('Data from server');
  }, 100);
}

// เคสทดสอบ
test('fetchData calls callback with correct data', (done) => {
  const mockCallback = jest.fn();
  fetchData(mockCallback);

  setTimeout(() => {
    expect(mockCallback).toHaveBeenCalledWith('Data from server');
    done();
  }, 200);
});

ในตัวอย่างนี้ `jest.fn()` จะสร้างฟังก์ชันจำลองที่มาแทนที่ฟังก์ชัน callback จริง การทดสอบจะตรวจสอบว่าฟังก์ชันจำลองถูกเรียกด้วยข้อมูลที่ถูกต้องโดยใช้ `toHaveBeenCalledWith()`

ตัวอย่างขั้นสูงที่ใช้โมดูล:

// user.js
import { getUserDataFromAPI } from './api';

export async function displayUserName(userId) {
  const userData = await getUserDataFromAPI(userId);
  return userData.name;
}

// api.js
export async function getUserDataFromAPI(userId) {
  // จำลองการเรียก API
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id: userId, name: 'John Doe' });
    }, 50);
  });
}

// user.test.js
import { displayUserName } from './user';
import * as api from './api';

describe('displayUserName', () => {
  it('should display the user name', async () => {
    // จำลองฟังก์ชัน getUserDataFromAPI
    const mockGetUserData = jest.spyOn(api, 'getUserDataFromAPI');
    mockGetUserData.mockResolvedValue({ id: 123, name: 'Mocked Name' });

    const userName = await displayUserName(123);
    expect(userName).toBe('Mocked Name');

    // คืนค่าฟังก์ชันดั้งเดิม
    mockGetUserData.mockRestore();
  });
});

ในที่นี้ `jest.spyOn` ถูกใช้เพื่อสร้างฟังก์ชันจำลองสำหรับฟังก์ชัน `getUserDataFromAPI` ที่นำเข้ามาจากโมดูล `./api` `mockResolvedValue` ถูกใช้เพื่อระบุค่าที่จะคืนกลับของ mock ส่วน `mockRestore` มีความสำคัญเพื่อให้แน่ใจว่าการทดสอบอื่น ๆ จะไม่ใช้เวอร์ชันที่ถูกจำลองโดยไม่ได้ตั้งใจ

Python กับ pytest และ unittest.mock

Python มีไลบรารีหลายตัวสำหรับการทำ mocking รวมถึง `unittest.mock` (ที่มีมาในตัว) และไลบรารีเช่น `pytest-mock` เพื่อการใช้งานที่ง่ายขึ้นกับ pytest

# ฟังก์ชันที่จะทดสอบ
def get_data_from_api(url):
    # ในสถานการณ์จริง ส่วนนี้จะทำการเรียก API
    # เพื่อความเรียบง่าย เราจะจำลองการเรียก API
    if url == "https://example.com/api":
        return {"data": "API data"}
    else:
        return None

def process_data(url):
    data = get_data_from_api(url)
    if data:
        return data["data"]
    else:
        return "No data found"

# เคสทดสอบโดยใช้ unittest.mock
import unittest
from unittest.mock import patch

class TestProcessData(unittest.TestCase):
    @patch('__main__.get_data_from_api') # แทนที่ get_data_from_api ในโมดูลหลัก
    def test_process_data_success(self, mock_get_data_from_api):
        # กำหนดค่า mock
        mock_get_data_from_api.return_value = {"data": "Mocked data"}

        # เรียกฟังก์ชันที่กำลังทดสอบ
        result = process_data("https://example.com/api")

        # ตรวจสอบผลลัพธ์
        self.assertEqual(result, "Mocked data")
        mock_get_data_from_api.assert_called_once_with("https://example.com/api")

    @patch('__main__.get_data_from_api')
    def test_process_data_failure(self, mock_get_data_from_api):
        mock_get_data_from_api.return_value = None
        result = process_data("https://example.com/api")
        self.assertEqual(result, "No data found")

if __name__ == '__main__':
    unittest.main()

ตัวอย่างนี้ใช้ `unittest.mock.patch` เพื่อแทนที่ฟังก์ชัน `get_data_from_api` ด้วย mock การทดสอบจะกำหนดค่า mock ให้คืนค่าที่ระบุ จากนั้นตรวจสอบว่าฟังก์ชัน `process_data` คืนผลลัพธ์ที่คาดหวัง

นี่คือตัวอย่างเดียวกันโดยใช้ `pytest-mock`:

# เวอร์ชัน pytest
import pytest

def get_data_from_api(url):
    # ในสถานการณ์จริง ส่วนนี้จะทำการเรียก API
    # เพื่อความเรียบง่าย เราจะจำลองการเรียก API
    if url == "https://example.com/api":
        return {"data": "API data"}
    else:
        return None

def process_data(url):
    data = get_data_from_api(url)
    if data:
        return data["data"]
    else:
        return "No data found"


def test_process_data_success(mocker):
    mocker.patch('__main__.get_data_from_api', return_value={"data": "Mocked data"})
    result = process_data("https://example.com/api")
    assert result == "Mocked data"


def test_process_data_failure(mocker):
    mocker.patch('__main__.get_data_from_api', return_value=None)
    result = process_data("https://example.com/api")
    assert result == "No data found"

ไลบรารี `pytest-mock` มี `mocker` fixture ที่ช่วยให้การสร้างและกำหนดค่า mock ภายใน pytest tests ง่ายขึ้น

Java กับ Mockito

Mockito เป็นเฟรมเวิร์กการทำ mocking ที่ได้รับความนิยมสำหรับ Java

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

interface DataFetcher {
    String fetchData(String url);
}

class DataProcessor {
    private final DataFetcher dataFetcher;

    public DataProcessor(DataFetcher dataFetcher) {
        this.dataFetcher = dataFetcher;
    }

    public String processData(String url) {
        String data = dataFetcher.fetchData(url);
        if (data != null) {
            return "Processed: " + data;
        } else {
            return "No data";
        }
    }
}

public class DataProcessorTest {

    @Test
    public void testProcessDataSuccess() {
        // สร้าง mock DataFetcher
        DataFetcher mockDataFetcher = mock(DataFetcher.class);

        // กำหนดค่า mock
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn("API Data");

        // สร้าง DataProcessor ด้วย mock
        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);

        // เรียกฟังก์ชันที่กำลังทดสอบ
        String result = dataProcessor.processData("https://example.com/api");

        // ตรวจสอบผลลัพธ์
        assertEquals("Processed: API Data", result);

        // ตรวจสอบว่า mock ถูกเรียกใช้งาน
        verify(mockDataFetcher).fetchData("https://example.com/api");
    }

    @Test
    public void testProcessDataFailure() {
        DataFetcher mockDataFetcher = mock(DataFetcher.class);
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn(null);

        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);
        String result = dataProcessor.processData("https://example.com/api");
        assertEquals("No data", result);
        verify(mockDataFetcher).fetchData("https://example.com/api");
    }
}

ในตัวอย่างนี้ `Mockito.mock()` จะสร้างอ็อบเจกต์จำลองสำหรับอินเทอร์เฟซ `DataFetcher` `when()` ถูกใช้เพื่อกำหนดค่าคืนกลับของ mock และ `verify()` ถูกใช้เพื่อตรวจสอบว่า mock ถูกเรียกด้วยอาร์กิวเมนต์ที่คาดหวัง

แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ฟังก์ชันจำลอง

ทางเลือกอื่นนอกเหนือจากฟังก์ชันจำลอง

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

สรุป

ฟังก์ชันจำลองเป็นเครื่องมือสำคัญสำหรับการเขียน unit tests ที่มีประสิทธิภาพ ช่วยให้คุณสามารถแยกส่วนประกอบ ควบคุมพฤติกรรม จำลองสภาวะข้อผิดพลาด และทดสอบโค้ดแบบอะซิงโครนัสได้ การปฏิบัติตามแนวทางที่ดีที่สุดและทำความเข้าใจทางเลือกอื่นๆ จะช่วยให้คุณสามารถใช้ประโยชน์จากฟังก์ชันจำลองเพื่อสร้างซอฟต์แวร์ที่แข็งแกร่ง เชื่อถือได้ และบำรุงรักษาง่ายขึ้น อย่าลืมพิจารณาข้อดีข้อเสียและเลือกเทคนิคการทดสอบที่เหมาะสมสำหรับแต่ละสถานการณ์เพื่อสร้างกลยุทธ์การทดสอบที่ครอบคลุมและมีประสิทธิภาพ ไม่ว่าคุณจะสร้างซอฟต์แวร์จากส่วนไหนของโลกก็ตาม