2019年4月28日 星期日

[Python] 使用unittest來測試程式碼

  Unit Test能幫助我們確保程式碼的正確性,協助我們進行debug,這篇我們要介紹在Python常使用的unittest來為我們的程式碼建立Unit Test,另外也有提到一些關於import module要注意的地方。

  專案架構:我們將test script統一放在tests這個資料夾中,tests是與src同級的目錄。(我使用的是Python36)
/proj
      - test.py
      - /src
            - __init__.py
            - calc.py
            - employee.py
      - /tests
            - __init__.py
            - test_calc.py
            - test_employee.py

src/calc.py:這就是我們要測試的程式碼
def add(a, b):
    return a+b
def substract(a, b):
    return a-b
def multiply(a, b):
    return a*b
def divide(a, b):
    if b==0:
        raise ValueError('Can not divide by zero!!')
    return a/b

tests/test_calc.py:
import unittest
from src import calc

class TestCalc(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
        self.assertEqual(calc.add(-1, 1), 0)

    def test_divide(self):
        self.assertEqual(calc.divide(4, 2), 2)
        self.assertEqual(calc.divide(-2, 1), -2)

        self.assertRaises(ValueError, calc.divide, 10, 0)

        with self.assertRaises(ValueError):
             calc.divide(10, 0)

  * 函式命名一定要以test為開頭!unittest才會將這個函式當作一組測試。
  * self.assertRaises(ValueError, calc.divide, 10, 0) 等同 with self.assertRaises(ValueError):  calc.divide(10, 0)

執行unittest:
  在/proj路徑下,執行 python -m unittest tests/test_calc.py
  也可以下這樣的命令 python -m unittest discover tests,這個指令會運行tests資料夾內所有的unittest
  (python -m unittest是指要load unittest module,後面接的是要給unittest的參數)

結果: "." 點的意思是成功,有兩個點,所以unit test是以TestCase裡的function為一單位。

  我們修改test_calc.py故意寫一個錯誤,F代表有錯:


 * 為什麼一定要在/proj路徑下指令?
    若是進入/proj/tests下這樣的指令 python -m unittest test_calc.py,會發現test_calc.py找不到src這個module!
    python -m unittest會以目前下指令的位置為top level的路徑,在test_calc.py中寫的是from src import calc,這是絕對路徑的寫法 ,絕對路徑是從top level的路徑出發,因此會若我們在/proj/tests下指令時會看/proj/tests下有無src module,在tests裡並沒有src所以才會報錯。
    (相對路徑會使用到".","."意思是與現在檔案同一目錄、".."則指前一個目錄、"..."前兩個目錄以此類推)



 

  那可不可以不要寫什麼 -m unittest呢?
  我只想打 python test_calc.py 。

  當然可以。  
 
  首先我們需要在test_cal.py加入if __name__ == '__main__',並運行unittest.main(),這個函式會運行main  module裡的所有test case。

  因為我們要執行的是這樣的命令 python test_calc.py,Python會以運行的檔案(__main__)位置做為top level路徑,能使用絕對路徑和相對路徑到達的只有在這個top level路徑下的東西。
  要想執行test_calc.py能開到上一層目錄的東西,就要將PROJECT_DIR加到sys.path中,告訴Python要找module還可以找這裡,如此test_calc.py中的from src import calc才不會找不到module。

  (Python找module會先找built-in module是否有該module,再來找sys.path這個list裡的目錄是否有該module,執行的檔案目錄會被加到sys.path中的第一順位)

import unittest
import sys
import os

TEST_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.abspath(os.path.join(TEST_DIR, os.pardir))
sys.path.insert(0, PROJECT_DIR)  ## "0"為最優先級

from src import calc

class TestCalc(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
        self.assertEqual(calc.add(-1, 1), 0)

    def test_divide(self):
        self.assertEqual(calc.divide(4, 2), 1)
        self.assertEqual(calc.divide(-2, 1), -2)

        self.assertRaises(ValueError, calc.divide, 10, 0)

        with self.assertRaises(ValueError):
            calc.divide(10, 0)

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

 
  那我們也可以做一個script運行tests資料夾內的所有Unit Tests。


proj/test.py:
import unittest

loader = unittest.TestLoader()
start_dir = 'tests/'  # path to test files
suite = loader.discover(start_dir)

runner = unittest.TextTestRunner()
runner.run(suite)

 
  命令只要執行 python test.py 就可以了!

  另外因為我們是執行 /proj 中的test.py,所以top level目錄是 /proj,那在我們的tests裡的那些test scripts就不需要將PROJECT_DIR加入到sys.path中,直接from src import 就可以了,因為只要在 /proj 下的都可以從 /proj出發以絕對路徑找到。




setUp() & tearDown():

 
  setUp()與tearDown()能幫助我們初始化測試所需的物件與清除(在python不需要自己刪除物件就是了),其他用法像是在setUp()登入資料庫,在tearDown()登出。
 
  直接看程式碼比較好了解。

src/employee.py:
class Employee():
    raise_amt = 1.05

    def __init__(self, name, pay):
        self.name = name
 self.pay = pay

    @property
    def email(self):
 return '{}@email.com'.format(self.name)

    def apply_raise(self):
 self.pay = int(self.pay*self.raise_amt)

tests/test_employee.py:
import unittest
import sys
import os

TEST_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.abspath(os.path.join(TEST_DIR, os.pardir))
sys.path.insert(0, PROJECT_DIR)

from src.employee import Employee


class TestEmployee(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("setupClass")

    @classmethod
    def tearDownClass(cls):
        print("teardownClass")

    def setUp(self):
        print("setUp")
        self.emp1 = Employee('Tim', 50000)
        self.emp2 = Employee('Andy', 60000)

    def tearDown(self):
        print("tearDown\n")

    def test_email(self):
        print("test_email")
        self.assertEqual(self.emp1.email, 'Tim@email.com')
        self.assertEqual(self.emp2.email, 'Andy@email.com')

    def test_apply_raise(self):
        print("test_apply_raise")
        self.emp1.apply_raise()
        self.emp2.apply_raise()

        self.assertEqual(self.emp1.pay, 52500)
        self.assertEqual(self.emp2.pay, 63000)
  

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


執行unittest:
  執行 python test_employee.py


結果:






參考資料:
  1. Python Tutorial: Unit Testing Your Code with the unittest Module  (youtube)

沒有留言:

張貼留言