Background

Test這塊這幾年有越來越被重視的感覺, 尤其是當CI的相關東西一直率續出來後. Unit Test感覺達到一個火紅的最高點. 那這邊主要是記錄如何使用Pytest來對python的程式做Unit Test. 並希望之後可以順利引進公司.

Prerequisites

首先我們先來建立一個python的virtual environment吧:

$ mkdir demo
$ cd demo
# create a virtual environment
$ virtualenv .venv
$ ls -a
.     ..    .venv
$ pip install pytest
$ pip list
appdirs (1.4.2)
packaging (16.8)
pip (9.0.1)
py (1.4.32)
pyparsing (2.1.10)
pytest (3.0.6)
setuptools (34.3.0)
six (1.10.0)
wheel (0.30.0a0)

.venv 是存放 virtual environment 的資料夾名稱

Basic

通常我是喜歡把Code放在同一個資料夾下, Test Code放在另外一個資料夾下. 主資料夾則是一個python的virtual environment.

所以通常基本的結構會長成這樣

$ tree
.
├── code
│   ├── demo.py
│   ├── __init__.py
├── requirements.txt
├── setup.py
└── tests
    └── test_demo.py

Pytest expects our tests to be located in files whose names begin with test_ or end with _test.py

所以基本上一個python檔案會對應到一個pytest的test檔案. (ex. demo.py => test_demo.py), 記得這樣的邏輯, 那我們就開始來創建相關的檔案吧.

code directory

  • __init__.py: 需要把整個資料夾變成一個module, 所以需要這個檔案. 那這個檔案通常填一些比較基本的資訊.
# -*- coding: utf-8 -*-

__version__ = '0.0.1'
__author__ = 'Paul Liang'
__email__ = 'paul@gmail.com'
__license__ = 'BSD'
__copyright__ = 'Copyright (c) 2017, Paul Liang.'
  • demo.py
# -*- coding: utf-8 -*-

def capital_case(x):
    return x.capitalize()

if __name__ == '__main__':  
    print(capital_case('semaphore'))

tests directory

  • test_demo.py
# -*- coding: utf-8 -*-

from code import demo

def test_capital_case():
    assert demo.capital_case('semaphore') == 'Semaphore'

assert 陳述在程式中安插除錯用的斷言(Assertion)檢查時很方便的一個方式

Testing

Basic

由於 pytest 會自己找到 tests/ 底下 test_*.py 裡頭所有 test_ 開頭的 function 作為 test cases.

$ python -m pytest
======================== test session starts ========================
platform linux -- Python 3.4.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
rootdir: /opt/paul/demo, inifile:
collected 1 items

tests/test_demo.py .

===================== 1 passed in 0.02 seconds =====================

如果我們故意把答案改成錯的 (ex: Semaphore => semaphore), 我們會得到下面的錯誤資訊:


===================== test session starts =====================
platform linux -- Python 3.4.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
rootdir: /opt/paul/demo, inifile:
collected 1 items

tests/test_demo.py F

===================== FAILURES =====================
--------------------- test_capital_case ---------------------
    def test_capital_case():
>       assert demo.capital_case('semaphore') == 'semaphore'
E       assert 'Semaphore' == 'semaphore'
E         - Semaphore
E         ? ^
E         + semaphore
E         ? ^

tests/test_demo.py:6: AssertionError

===================== 1 failed in 0.04 seconds =====================

Test Coverage

如果我們還想知道測試涵蓋多少程式碼. 這時候就需要用到 pytest-cov 這個 pytest 的 plug-in:

$ pip install pytest-cov

然後只要在指令後面加上 --cov [MODULE_NAME] 就可以測試 coverage 了:

$ python -m pytest --cov code
===================== test session starts =====================
platform linux -- Python 3.4.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
rootdir: /opt/paul/demo, inifile:
plugins: cov-2.4.0
collected 1 items

tests/test_demo.py .

----------- coverage: platform linux, python 3.4.3-final-0 -----------
Name               Stmts   Miss  Cover
--------------------------------------
code/__init__.py       5      5     0%
code/demo.py           4      1    75%
--------------------------------------
TOTAL                  9      6    33%


===================== 1 passed in 0.03 seconds =====================

如果想知道程式有哪幾行沒測到,可以在指令中加上 --cov-report term-missing:

20:48:29 › python -m pytest --cov-report term-missing --cov code
===================== test session starts =====================
platform linux -- Python 3.4.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
rootdir: /opt/paul/demo, inifile:
plugins: cov-2.4.0
collected 1 items

tests/test_demo.py .

----------- coverage: platform linux, python 3.4.3-final-0 -----------
Name               Stmts   Miss  Cover   Missing
------------------------------------------------
code/__init__.py       5      5     0%   3-7
code/demo.py           4      1    75%   8
------------------------------------------------
TOTAL                  9      6    33%


===================== 1 passed in 0.02 seconds =====================

接著也可以在程式中用註解來指定某個部分不要加入到 coverage 的計算:

# demo/demo.py
# ...
if __name__ == '__main__':  # pragma: no cover  
    print(capital_case('semaphore'))

and try again,

$ python -m pytest --cov-report term-missing --cov code
===================== test session starts =====================
platform linux -- Python 3.4.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
rootdir: /opt/paul/demo, inifile:
plugins: cov-2.4.0
collected 1 items

tests/test_demo.py .

----------- coverage: platform linux, python 3.4.3-final-0 -----------
Name               Stmts   Miss  Cover   Missing
------------------------------------------------
code/__init__.py       5      5     0%   3-7
code/demo.py           2      0   100%
------------------------------------------------
TOTAL                  7      5    29%


===================== 1 passed in 0.03 seconds =====================

最後Pytest的基本功能大致上就差不多是這樣.

Reference