1. Python unittest คืออะไร?

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

ความสำคัญของการทดสอบหน่วย

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

2. วิธีการใช้ unittest เบื้องต้น

unittestพื้นฐานคือการสร้างคลาสที่สืบทอดจาก unittest.TestCase แล้วกำหนดเมธอดทดสอบภายในคลาสนั้น ในเมธอดทดสอบจะใช้เมธอด assertion เช่น assertEqual() เพื่อเปรียบเทียบผลลัพธ์ที่คาดหวังกับผลลัพธ์จริง

ตัวอย่างการทดสอบพื้นฐาน

โค้ดต่อไปนี้เป็นตัวอย่างง่าย ๆ ที่ทดสอบฟังก์ชัน add(a, b)
import

# โค้ดที่ต้องทดสอบ
def add(a, b):
    return a + b

# คลาสทดสอบ
class TestAddFunction(unittest.TestCase):

    def test_add_integers(self):
        result = add(2, 3)
        self.assertEqual(result, 5)

if __name__ == '__main__':
    unittest.main()
ในโค้ดนี้ทำการทดสอบว่า ฟังก์ชัน add() ทำงานอย่างถูกต้องหรือไม่ เมธอด assertEqual() ตรวจสอบว่าค่าที่คาดหวังและผลลัพธ์จริงเท่ากัน วิธีนี้ช่วยยืนยันว่าฟังก์ชันทำงานถูกต้องในหลายกรณี

การขยายการทดสอบ

สามารถใช้เมธอดทดสอบหลาย ๆ ตัวเพื่อทดสอบการทำงานของฟังก์ชันกับอินพุตที่หลากหลาย เช่น การทดสอบจำนวนทศนิยมหรือการต่อสตริง
def test_add_floats(self):
    result = add(2.5, 3.5)
    self.assertAlmostEqual(result, 6.0, places=2)

def test_add_strings(self):
    result = add("Hello, ", "World!")
    self.assertEqual(result, "Hello, World!")
ด้วยวิธีนี้ การทดสอบการทำงานของฟังก์ชันกับประเภทข้อมูลที่ต่างกันก็ช่วยยืนยันว่าฟังก์ชันทำงานถูกต้องในสถานการณ์ต่าง ๆ

3. วิธีการใช้ setUp() และ tearDown()

เพื่อให้ดำเนินการบางอย่างโดยอัตโนมัติก่อนและหลังการทดสอบ ให้ใช้เมธอด setUp() และ tearDown() ซึ่งจะทำให้สามารถเตรียมการที่จำเป็นก่อนการรันเทสต์และทำความสะอาดหลังจากเทสต์เสร็จสิ้นได้

ตัวอย่าง setUp()

setUp() เมธอดเป็นเมธอดที่เรียกใช้เสมอก่อนแต่ละเมธอดทดสอบ ทำให้สามารถรวมขั้นตอนการเริ่มต้นที่ใช้ร่วมกันได้
def setUp(self):
    self.temp_value = 42

ตัวอย่าง tearDown()

tearDown() เมธอดจะทำงานหลังจากแต่ละเมธอดทดสอบ เพื่อทำการทำความสะอาดหรือปล่อยทรัพยากร เช่น การตัดการเชื่อมต่อฐานข้อมูลหรือการลบไฟล์ชั่วคราว
def tearDown(self):
    self.temp_value = None
ด้วยวิธีนี้จะช่วยลดความซ้ำซ้อนของโค้ดทดสอบและรักษาโค้ดให้สะอาดยิ่งขึ้น

4. การทดสอบความขึ้นอยู่ด้วยการใช้ Mock

เมื่อโค้ดที่ต้องการทดสอบพึ่งพาแหล่งข้อมูลภายนอก(ฐานข้อมูล、API เป็นต้น)ในกรณีที่พึ่งพานั้นถูกแทนที่ด้วย mock จะช่วยปรับปรุงความเร็วในการรันเทสต์และทำให้เทสต์สามารถคาดการณ์ได้ หากใช้โมดูล unittest.mock ของ Python ก็สามารถทำได้อย่างง่ายดาย

ตัวอย่างการใช้ Mock

ในโค้ดต่อไปนี้ ฟังก์ชันที่ใช้เวลานาน time_consuming_function() ถูกแทนที่ด้วย mock
from unittest.mock import patch

class TestAddFunction(unittest.TestCase):

    @patch('my_module.time_consuming_function')
    def test_add_with_mock(self, mock_func):
        mock_func.return_value = 0
        result = add(2, 3)
        self.assertEqual(result, 5)
ในตัวอย่างนี้ เราใช้ mock เพื่อทำการทดสอบโดยไม่เรียก time_consuming_function ซึ่งช่วยลดเวลาในการทดสอบในขณะเดียวกันก็ได้ผลลัพธ์ที่แม่นยำ
侍エンジニア塾

5. การจัดการข้อยกเว้นและการอ้างอิงแบบกำหนดเอง

unittest ในสามารถทดสอบการจัดการข้อยกเว้นได้เช่นกัน ตัวอย่างเช่น เพื่อตรวจสอบว่าข้อยกเว้นเกิดขึ้นอย่างถูกต้องในสถานการณ์เฉพาะ ให้ใช้ assertRaises() เพื่อทำเช่นนั้น

การทดสอบการจัดการข้อยกเว้น

ในตัวอย่างต่อไปนี้ จะตรวจสอบว่ามีการเกิด ZeroDivisionError
def test_divide_by_zero(self):
    with self.assertRaises(ZeroDivisionError):
        divide(1, 0)
โค้ดนี้ทดสอบว่าเมื่อเรียก divide(1, 0) จะเกิด ZeroDivisionError

การสร้างการอ้างอิงแบบกำหนดเอง

ในกรณีที่การอ้างอิงมาตรฐานไม่สามารถรองรับได้ สามารถสร้างเมธอดการอ้างอิงแบบกำหนดเองของคุณเองได้
def assertIsPositive(self, value):
    self.assertTrue(value > 0, f'{value} is not positive')
โดยใช้การอ้างอิงแบบกำหนดเอง คุณสามารถรองรับสถานการณ์การทดสอบที่เฉพาะเจาะจงมากขึ้น

6. ฟีเจอร์การค้นหาเทสต์ของ unittest

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

วิธีใช้การค้นหาเทสต์

เพื่อรันการค้นหาเทสต์ ให้ใช้คำสั่งต่อไปนี้
python -m unittest discover
ด้วยคำสั่งนี้ จะทำการรันไฟล์ test_*.py ทั้งหมดในไดเรกทอรีที่ระบุ หากต้องการระบุไฟล์หรือไดเรกทอรี ให้ใช้ตัวเลือกดังต่อไปนี้
python -m unittest discover -s tests -p "test_*.py"
ด้วยฟีเจอร์นี้ คุณสามารถประหยัดความยุ่งยากในการระบุไฟล์เทสต์แต่ละไฟล์ได้ และสามารถจัดการเทสต์อย่างมีประสิทธิภาพแม้ในโปรเจกต์ขนาดใหญ่

7. เคล็ดลับการเพิ่มประสิทธิภาพด้วย unittest

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

เพิ่มประสิทธิภาพไฟล์ I/O

เทสต์ที่ต้องอ่านเขียนไฟล์สามารถเร่งความเร็วได้โดยประมวลผลในหน่วยความจำ ใช้ StringIO เพื่อสร้างอ็อบเจ็กต์ที่ทำหน้าที่เหมือนไฟล์ในหน่วยความจำ ทำให้หลีกเลี่ยงการทำ I/O กับดิสก์。
from io import StringIO

class TestFileOperations(unittest.TestCase):

    def test_write_to_memory(self):
        output = StringIO()
        output.write('Hello, World!')
        self.assertEqual(output.getvalue(), 'Hello, World!')
ด้วยวิธีนี้ แม้เทสต์ที่ต้องเข้าถึงไฟล์ก็สามารถปรับปรุงความเร็วของเทสต์ได้อย่างมาก。

ใช้ Mock

เพื่อจำกัดการเข้าถึงทรัพยากรภายนอก เราสามารถใช้ Mock เพื่อเร่งความเร็วของเทสต์ได้ ซึ่งช่วยหลีกเลี่ยงความล่าช้าจากเครือข่ายฐานข้อมูลและลดเวลาการรันเทสต์ ตัวอย่างต่อไปนี้จะแทนการเรียก API ด้วย Mock。
from unittest.mock import MagicMock

class TestApiCall(unittest.TestCase):

    def test_api_response(self):
        mock_api = MagicMock(return_value={'status': 'success'})
        response = mock_api()
        self.assertEqual(response['status'], 'success')
ด้วยวิธีนี้ การทดสอบฟังก์ชันโดยไม่พึ่งพาทรัพยากรภายนอกจะทำให้สร้างสภาพแวดล้อมการทดสอบที่เร็วขึ้นและเสถียรยิ่งขึ้น。

8. สรุปและขั้นตอนต่อไป

ในบทความนี้ เราได้อธิบายอย่างกว้างขวางตั้งแต่พื้นฐานของการทดสอบหน่วยด้วย unittest ของ Python การใช้การตั้งค่าและการทำความสะอาด การทดสอบความขึ้นอยู่ด้วย mock ไปจนถึงเทคนิคการปรับปรุงประสิทธิภาพของการทดสอบ

สรุปประเด็นสำคัญ

  • วิธีการใช้พื้นฐาน: สืบทอดจาก unittest.TestCase และใช้เมธอด assertion เพื่อสร้างการทดสอบ
  • setUp() / tearDown(): การจัดการกระบวนการทั่วไปก่อนและหลังการทดสอบช่วยเพิ่มการนำโค้ดกลับมาใช้ใหม่และความอ่านง่ายของโค้ด
  • การใช้ mock: สามารถทดสอบฟังก์ชันโดยไม่ต้องพึ่งพาแหล่งข้อมูลภายนอก ทำให้ประสิทธิภาพของการทดสอบเพิ่มขึ้นอย่างมาก
  • การค้นพบการทดสอบ (Test Discovery): ฟีเจอร์ที่สะดวกช่วยให้การจัดการการทดสอบในโครงการขนาดใหญ่ง่ายขึ้น
  • เทคนิคการปรับปรุงประสิทธิภาพ: การประมวลผลในหน่วยความจำและการใช้ mock สามารถลดเวลาการรันของการทดสอบได้

ขั้นตอนต่อไป

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