Skip to content

การทำ TDD สำหรับ Python

Last updated on July 15, 2015

ภาษาโปรแกรมสมัยใหม่ (หมายถึงยุคนี้ซึ่งอาจจะมีอายุ 10-20 ปีแล้วก็ได้) บางภาษาทุกวันนี้มักจะ built-in ไลบรารี่ unit testing framework มาให้อยู่แล้วอย่างเช่น Golang Python เองก็มีมาให้ด้วยเหมือนกัน

โพสต์นี้ผมอธิบายเป็น step ตามนี้ละกันครับ

1. เตรียมเครื่องมือที่จำเป็นก่อน

เนื่องจากผมใช้ Ubuntu ฉะนั้นเครื่องไม้เครื่องต่างๆ จะเป็นทางฝั่งลินุกซ์นะครับ OS อื่นๆ ก็หามาติดตั้งเองครับไม่น่ายากสำหรับ developer อย่างเรา

1.1 ติดตั้ง Editor/IDE

มีให้เลือกใช้เยอะแยะตั้งแต่ editor ธรรมดาและ IDE ซึ่งมีเครื่องไม้เครื่องมีในการพัฒนามาให้ตั้งแต่เริ่มต้น เลือกใช้กันตามสะดวก ใครใช้ editor ก็เลือกที่ highlight โค้ดได้ก็ดีครับจะได้ดูง่ายๆ

ในตัวอย่างนี้ผมจะใช้ editor ธรรมดาคู่กับการรันคำสั่งจาก terminal ครับ

1.2 ติดตั้ง Python

สำหรับ Python Ubuntu มีให้อยู่แล้วไม่ต้องติดตั้งเพิ่มอีก ใช้ Python 2 หรือ Python 3 ก็ได้ครับ

ถ้าใช้ Ubuntu อยู่ ข้ามไปเลยก็ได้ครับ

1.3 [Optional] ติดตั้ง Pylint

ภาษาโปรแกรมแต่ละภาษาจะมีมาตรฐานการเขียนของมันอยู่ เช่น การย่อหน้า, การตั้งชื่อตัวแปร, การเว้นวรรค-บรรทัด, การเขียน document ประกอบคลาส, เมธอด

Python เองก็มีเช่นกันครับ Pylint นี้จะช่วยตรวจสอบว่าโค้ดของเราถูก format มาตรฐานตามที่ develop ทั่วไป (โครงการ Open source) เขายอมรับหรือไม่, ช่วยหาตัวแปรที่ไม่ถูกใช้, ดูว่าคลาส/ฟังก์ชันของเราซับซ้อนขนาดไหน แนะนำว่าควรใช้ครับโค้ดของเราจะได้สวยงามขึ้น

การติดตั้ง Pylint เนื่องจากยังไม่มี package ใน repository ของ ubuntu ให้เราติดตั้ง PIP ซึ่งเป็น package installation ของ Python ก่อนจากนั้นค่อยติดตั้ง Pylint ครับ

sudo apt-get install build-essential python3-dev python3-pip 
sudo pip install pylint

หมายเหตุ: การติดตั้งด้วย pip เป็นการดาวน์โหลดซอร์สโค้ดมาคอมไพล์แล้วติดตั้งหากเกิดปัญหาติดตั้งแล้ว error ให้ติดตั้ง package build-essential และ python3-dev  ไปด้วย

1.4 [Optional] ติดตั้ง Nosetest3

ช่วยในการรันเทสให้ง่ายขึ้น ไม่ต้องพิมพ์คำสั่งยาวๆ อย่าง python -m unittest [PATH.TO.MODULE]  อีกต่อไปแค่สั่ง nosetest3  มันจะวิ่งหาไฟล์ที่มีเทสเคสอยู่ให้

Nose สำหรับ Python3 มีอยู่ใน repository แล้วติดตั้งผ่าน APT ได้เลย

sudo apt-get install python3-nose

2. สร้างไดเรกทอรีของโปรเจกต์

ให้สร้างโครงสร้างไดเรกทอรีตามนี้ครับ

-project/
    -lib/
    -tests/
    -main.py

ที่จำเป็นคือ lib/  และ tests/  โครงสร้างไดเรกทอรีอย่างน้อยๆ ก็เป็นไปตามนี้ครับส่วนใครจะเพิ่ม bin/  doc/  data/  อะไรก็แล้วแต่ครับหน้าที่ของแต่ละไดเรกทอรีก็ตามแต่เราจะแยก เช่น

  • lib/  สำหรับคลาส/ฟังก์ชันของโปรแกรม
  • tests/  สำหรับเทสที่เราเขียนขึ้นมา
  • bin/  สำหรับไบนารีไฟล์หรือสคริปต์ที่ช่วยอำนวยความสะดวกต่างๆ
  • doc/  สำหรับ documents ของโปรแกรม
  • data/  สำหรับเก็บ data ภายนอกที่ใช้ในโปรแกรม เช่น ไฟล์ข้อมูลแบบ text, ไฟล์ฐานข้อมูล

สำหรับไดเรกทอรีที่เราจำเป็นต้อง import เข้ามาใช้งาน เช่น ไดเรกทอรี lib/ จำเป็นต้องมีไฟล์ __init__.py  ด้วยเสมอครับ เป็นสร้างเป็นไฟล์เปล่าก็ได้ (รายละเอียดอื่นๆ ลองไปหาอ่านดูเองนะ) ก็จะได้โครงสร้างของไดเรกทอรีประมาณนี้

-project/
    -lib/
        -__init__.py
        -awesome.py
    -tests/
        -awesome_test.py
    -main.py

3. เริ่มต้นด้วยการเขียนเทสก่อน

เสร็จแล้วก็เริ่มเขียนเทสก่อนเลยครับ

tests/awesome_test.py

import unittest
from lib import awesome

class AwesomeTest(unittest.TestCase):
    def setUp(self):
        self.awesome = awesome.Awesome()

    def test_printHelloShouldReturnHelloWorld():
        expect = 'Hello World'
        actual = self.awesome.hello()
        self.assertEqual(actual, expect)

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

*หมายเหตุ: ชื่อเมธอดต้องขึ้นต้นด้วย test_ เสมอ unittest จึงจะมองเห็น

เขียนโค้ดเสร็จอย่าลืมรัน Pylint ดูด้วยนะครับว่ามีตรงไหนเขียนไม่ถูกมาตรฐานหรือเปล่าโดยใช้คำสั่ง

pylint tests/awesome_test.py

เสร็จแล้วลองรันเทสดูครับ ซึ่งแน่นอนว่ามันจะ fail แดงเถือกเลย (แหงหละ) ให้เราแก้ตามที่ error มันฟ้อง การรันเทสใช้คำสั่ง

cd /PATH/TO/project
python -m unittest tests.awesome_test

หรือถ้าต้องการเทสทั้งไดเรกทอรี tests/ ก็สั่งแบบนี้

python -m unittest tests

หรือถ้าใช้ Nosetest ก็รันคำสั่งได้เลย

nosetest3

4. เขียนโค้ดเพื่อทำให้ error ที่เกิดขึ้นหายไป

lib/awesome.py

class Awesome():
    def hello():
        return 'Hello Worl'

จากตัวอย่างนี้คลาส Awesome มีเมธอดเดียวคือ hello() ซึ่งจะ return string ‘Hello World’ ออกมา

5. รันเทสอีกครั้ง

จากข้อ 4 ผมตั้งใจวางบั๊กไว้ คือลบตัว ‘d’ ในคำว่า ‘World’ ออกไปเพื่อให้เทสไม่ผ่าน เมื่อเรารันเทสจะพบว่าเกิด error ขึ้นเพราะสิ่งที่เราคาดไว้ (expect) กับความจริงที่เกิดขึ้น (actual) มันไม่เท่ากันจะเกิด error ประมาณนี้ออกมาครับ

..F.
======================================================================
FAIL: printHelloShouldReturnHelloWorld (tests.awesome_test.AwesomeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/PATH/TO/project/tests/awesome_test.py", line 8, in printHelloShouldReturnHelloWorld
    self.assertEqual(actual, expect)
AssertionError: Strings differ: 'Hello Worl' != 'Hello World'

...

- 'Hello Worl'
?            

+ 'Hello World'
?            ^


----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)

assert method ไม่ได้มีแค่ assertEqual นะครับยังมี method อื่นให้ใช้อีกเยอะเลย เข้าไปดู assert method ทั้งหมดใน document ของ module unittest ได้ครับ

6. ถ้ามีบั๊กอยู่ให้แก้ไขแล้วเทสใหม่จนกว่าจะผ่าน

ให้เราแก้ไข hello() ให้ถูกโดยการเพิ่ม ‘d’ เข้าไป แล้วรันเทสอีกครั้ง เทสควรผ่านและมีข้อความตามนี้ออกมา

....
----------------------------------------------------------------------
Ran 1 test in 0.008s

OK

เสร็จแล้วครับ สนุกเหมือนเล่นเกมเลยเนอะ

เวลาจะเอาโค้ดไปใช้งานเครื่องอื่นหรือคนอื่นเอาไปพัฒนาต่อก็รันเทสดูก่อนถ้า fail ก็แก้เป็นเคสๆ ไปจนผ่านครับ

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.