มาเขียน Unit Test ใน Python กัน

แนวคิดการเขียนโค้ดเพื่อทดสอบโค้ดของเรานั้นอาจจะไม่ใช่แนวคิดที่ใหม่แต่อย่างใด แต่การเขียนโค้ดเพื่อทดสอบ “ก่อน” เขียนโค้ด (Test-Driven Development หรือ TDD) ยังคงเป็นเรื่องใหม่สำหรับหลายๆ คน ปัญหาหลักอีกอย่างหนึ่งก็คงเป็นเพราะเราไม่รู้ว่าจะเริ่มต้นเขียนอย่างไร เราจะมาเริ่มต้นเขียนโค้ดเพื่อทดสอบ และเขียน “ก่อน” ที่เราจะเขียนโค้ดเพื่อทำงานจริงๆ กันโดยใช้ Python ในบทความนี้

สำหรับคนที่ใช้ Mac หรือพวก Linux ต่างๆ Python จะติดตั้งมาให้เรียบร้อยแล้ว ไม่ต้องลงอะไรเพิ่ม ส่วนคนที่ใช้ Windows ต้องไปโหลดมาก่อนนะครับที่ Python Releases for Windows

สมมุติว่าเราต้องการที่จะกรองตัวเลขออกมาจากข้อความใดๆ เช่น ข้อความ “aa1bb2cc3dd” ผลลัพธ์ที่ได้คือ 123 ก่อนอื่นให้เราสร้างไฟล์ filter_string.py ขึ้นมา แล้วเขียนตามนี้

import unittest


class FilterStringTest(unittest.TestCase):
    def test_fail(self):
        self.assertTrue(False)


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

บรรทัดแรกเป็นการนำโมดูล Unit Test Framework ชื่อ unittest เข้ามาใช้ โมดูลนี้จะช่วยให้เราเขียนโค้ดทดสอบได้สะดวกมากขึ้น เราแค่เขียนโค้ดทดสอบไปในแต่ละกรณี แล้วพอสั่งคำสั่ง python กับไฟล์นี้ ตัวโมดูลจะเข้าไปอ่านโค้ดทดสอบของเราแล้วก็จะนำไปประมวลผลให้เรา เรานั่งรอดูผลลัพธ์เท่านั้น

บรรทัดในส่วนของ class ให้เรามองเป็นชุดทดสอบของเรา และภายในชุดทดสอบก็จะมีหลายๆ ตัวทดสอบ ในกรณีนี้เรามีชุดทดสอบของโปรแกรมกรองตัวเลขอยู่ชุดหนึ่ง แล้วก็มีตัวทดสอบ fail อยู่ข้างใน

ส่วนบรรทัด if __name__ == '__main__': เป็นทริกครับ ค่า __name__ ในโมดูลจะถูกเซตตามวิธีที่เราใช้งาน ถ้าเรา import เข้ามา ค่า __name__ จะเป็นชื่อโมดูลนั้นๆ แต่ถ้าเราสั่งคำสั่งกับโมดูลนั้นโดยตรง ค่า __name__ จะเป็น __main__ ครับ ซึ่งปกติเราจะเขียนไฟล์โค้ดสำหรับทดสอบแยกไว้ แล้วก็สั่งคำสั่งแยกจากโค้ดที่ใช้ทำงานจริง ดังนั้นเวลาที่เราอยากสั่งคำสั่งกับโค้ดทดสอบ ให้เราสั่งงานกับโค้ดนั้นโดยตรงได้เลย

ให้เราลองสั่ง python filter_string.py เลย เราควรจะได้ผลลัพธ์แบบนี้

➜  ~  python filter_string.py
F
======================================================================
FAIL: test_fail (__main__.FilterStringTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "filter_string.py", line 6, in test_fail
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

เราได้ F ออกมาแปลว่าตัวทดสอบของเรา fail ครับ ณ จุดนี้เราก็พร้อมที่จะเริ่มต้นเขียนชุดทดสอบของเราจริงๆ กันแล้วครับ 🙂

เรากลับไปเขียนตัวทดสอบของเราให้มันดีๆ กันดีกว่า ผมขอเปลี่ยนตัวทดสอบ test_fail เป็น test_input_containing_only_digits_should_get_only_digits ตามนี้

import unittest


class FilterStringTest(unittest.TestCase):
    def test_input_containing_only_digits_should_get_only_digits(self):
        result = filter_string('123')
        self.assertEqual(result, 123)


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

ถ้าเราสั่ง python filter_string.py เราควรจะได้แบบนี้

➜  ~  python filter_string.py
E
======================================================================
ERROR: test_input_containing_only_digits_should_get_only_digits (__main__.FilterStringTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "filter_string.py", line 6, in test_input_containing_only_digits_should_get_only_digits
    result = filter_string('123')
NameError: global name 'filter_string' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

ได้ E แปละว่า error ครับ เรายังไม่มีฟังก์ชั่น filter_string เลย ต่อไปเราก็ไปสร้างฟังก์ชั่นนี้ซะ ผมขอสร้างไว้ที่ไฟล์เดียวกันเลยนะครับ ตามนี้

import unittest


class FilterStringTest(unittest.TestCase):
    def test_input_containing_only_digits_should_get_only_digits(self):
        result = filter_string('123')
        self.assertEqual(result, 123)


def filter_string(text):
    return 123


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

พอเราสั่ง python filter_string.py อีกที เราจะได้

➜  ~  python filter_string.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

แบบนี้แปลว่าโค้ดทดสอบของเราผ่าน โค้ดทำงานได้ตามที่เราคาดหมายไว้ครับ

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

ที่เหลือผมขอให้เป็นงานของผู้ที่หลงเข้ามาอ่านบทความนี้นะครับ ช่วยทำโปรแกรมของผมให้สมบูรณ์ที

ขอบคุณครับ 🙂