TDD 與 CI 在 Python 中的實踐


社區化產品的長久生存之道可能莫過於對迭代周期的控制。還記得以前采用老土的階段開發的年代,將軟件生命周期分為各個階段,當到達每個階段的里程碑則集中所有的資源、人力作全面沖刺。每次到了里程碑的檢查點沖過了就可以集體慶功,沖爬下了就集體加班。而后者發生的機率總是比前者要多,現在回想起來真有種大浪淘沙,不堪回首之感。

現在 敏捷開發 用順溜了,回過頭來看這種作坊式的開發甚是感觸。階段式的開發本身並無問題,而是迭代周期的控制很容易出錯。往往都會將階段周期拉得很長,盡量在每個階段內將所有的工作完善之后再進入下一周期。然而,千里之堤,潰於蟻穴,過長的周期往往不會按我們預期的想法而進行,總是出現各種的問題,歸結原因更多的是因為風險疊加的結果。優秀的PM會有N種處理風險的手段與經驗,而且關於風險控制的理論層出不窮,這類的課程也是一掃一大堆。不過再強的PM再優秀的PM也架不住風險在里程碑的集中性爆發。

這可能是也是 敏捷開發 最吸引人的地方,因為風險的集中性爆發的影響被 持續集中 CI 給最小化了。本文的主題並不是全面地討論敏捷的理論,我相信有敏捷開發實踐的人並不在少數,真正驅動我寫下本文的動力是自從.net 移居到 linux 這個大世界后發現持續集成是如此的簡單,執行的成本是如此之低,各種敏捷的工具可謂一應俱全,很想將這個過程記錄下來以供分享。

測試驅動 TDD

最近,在完成FreezesBeta版的開發,我就遇到發布問題,在微軟平台上輕車熟路的做法現在得重新適應。Python 之所以誘惑人可能是她總是能給人驚喜吧。

多年強制實踐敏捷的好處是可以徹底改掉不寫測試的壞毛病,當測試寫多了會自然萌生一種“不寫測試就不安心”的感覺。 Python 世界有很優秀的的測試框架,例如:unittest, pyTest, nose, doctest 等等。由於 unittest 是內置框架,而且本人也比較懶所以很長時間內我也沒用采用其它的測試框架,直至最近才發現 nose 在這諸多測試框架中的便利性,而且可以完全與 unittest 兼容還帶有大量的代碼斷言工具,實在是很不錯。關於 nose 的使用心得可以參考我發布在自己博客上的使用筆記:Nose 測試框架

我認為實踐持續集成的核心就是TDD而不是小版本,因為通過測試就是驗證小版本可發布的唯一標准。在體驗 python TDD過程中不得不為 pyCharm 4 這個工具點贊!由其是當全面運行覆蓋率檢測時,pyCharm4已將UI與 coverage 很好的集成在一齊,可以很方便地查看項目中代碼的測試覆蓋情況:

另外在構建python測試有幾個十分實用的工具

  • coverage - 生成代碼測試覆蓋率
  • ForgeryPy - 數據偽造工具
  • selenium - 著名的E2E測試服務器
  • mock - 對象、接口偽裝工具

關於 TDD 的基礎理論在此不作贅述有興趣的朋友可以去度娘或者谷歌。

Python 發布包

當所有的測試通過后,一下步就是小版本的發布了。現在幾乎沒有什么開發語言體系是不具備官方的依賴包引用庫的了,用 python 的話當然需要將可運行代碼發布到 pypi 上,其它用戶就能通過本地的命令行工具 pip 直接安裝了(相當於做C#開發時直接從Nuget直接下載依賴包一樣)。

python 的安裝包是需要通過 setuptools 工具打包,生成 egg-info 和 發布包的。在代碼中唯一需要做的工作就是編寫 setup.py 文件。這個過程其實是瞞坑爹的,因為在python的包管理工具除了 setuptools 這個歷史最為悠久的還有新一代的 distutils 工具,而官方說明非常地詳細,具體可以參考Python Packaging User Guide 。 我花了老半天才將這份官方文檔全部讀完,但坑爹的是實作過程根本沒有這么復雜,所以我在此總結了一下:

首先,在編寫setup.py 之前需要一份依賴包引用文件 requirements.txt,(如果有就跳到下一步),在當前目錄下進入命令行執行:

$ pip freeze 

這里是 pip 的詳細使用文檔

執行成功后將會自動產生 requirements.txt。如果你不做這一步那么只能在 setup.py 內手工寫 install_requires 了。

是在項目根目錄下建立 setup.py文件,最簡單的做法如下:

import os
import re
from setuptools import setup, find_packages

# 讀入依賴引用文件
with open('requirements.txt') as reqs_file:
    REQS = reqs_file.read()

setup(
    name='項目名', # Pypi 顯示的項目名
    version='1.0',
    packages=find_packages(exclude=['tests']), 
    install_requires=REQS, # 指定 setup 運行時的依賴包
)

這里有兩個重點,一個是 find_packages,這個方法會在 setup.py 執行時將所有的 python 包(必須帶有__init__.py)和包內的 .py 文件添加到打包目錄中, 另一個就是 install_requires 這是 setup.py 在運行時自動檢查環境內是否具備指定的依賴,如果沒有就會自動下載安裝。

寫完 setup.py 就可以在命令行執行測試了

$ python setup.py build

注意,此處我並沒有直接執行install 而只是使用 build 先將發布包生成至 build 目錄並且輸出 egg-info。通過這一步可以先檢查最終發布包中是否有文件缺失。

如果 python 項目中包含有源碼文件以外的資源需要打包發布,那么可以使用package_data屬性,這個屬性是一個“字典”類型,鍵(Key)值用於指定路徑(當前項目路徑是空串)。值(Value) 是一個文件數組,指定包含的文件資源的匹配表達式。如果是 Flask 的標准項目結構,要將 statictemplates 的內容包含於發布包內,那么:

#...

setup(
    #...
    package_data={
        '': ['*.*', # 根目錄下所有的文件類型
             'static/**/*.*',   # static 目錄下所有的子目錄及所有文件
             'templates/*.*',   # tempaltes 目錄下所有的文件
             'templates/**/*.*' # tempaltes 目錄下所有子目錄及所有文件
        ]
    },
    #...
)

以下是整個項目的完整 setup.py 文件

import os
import re
from setuptools import setup, find_packages

## 從源碼目錄中讀取頂層包的 __version__ ,以便以后版本的統一更改
with open(os.path.join(os.path.dirname(__file__),
                       '這里是源碼目錄名', '__init__.py')) as init_py:
    VERSION = re.search("VERSION = '([^']+)'", init_py.read()).group(1)

# 讀入依賴引用文件
with open('requirements.txt') as reqs_file:
    REQS = reqs_file.read()

setup(
    name='Freezes',
    version=VERSION,
    packages=find_packages(exclude=['tests']), 
    install_requires=REQS, # 指定 setup 運行時的依賴包
    package_data={
        '': ['*.yml',
             '*.json',
             '*.cfg',
             'layouts/*',
             'seeds/*',
             'static/**/*.*',
             'templates/*.*',
             'templates/**/*.*',
             'translations/*.*'
        ]
    },
    entry_points={
        'console_scripts': [
            'freezes=freezes.server:main'
        ],
        'setuptools.installation': [
            'eggsecutable = freezes.server:main'
        ]
    },
    ## 以下內容是可選的,用於生成 egg-info 的內容
    url='http://freezes.dotnetage.com',
    license='BSD',
    author='Ray',
    author_email='csharp2002@hotmail.com',
    description="這里是項目簡述,會在pipy的項目列表中顯示",
    long_description="這里是項目的詳細描述,會在pypi項目詳細頁面中顯示",
    zip_safe=False,
    platforms='any',
    keywords=('static', 'flask'),
    classifiers=['Development Status :: 4 - Beta',
                 'Intended Audience :: Developers',
                 'License :: OSI Approved :: BSD License',
                 'Natural Language :: English',
                 'Operating System :: OS Independent',
                 'Programming Language :: Python :: 2.7',
                 'Topic :: Software Development :: Libraries',
                 'Topic :: Utilities']
)

到此,貌似所有的准備工作已准備完成,但恰恰這里可能就有個坑,我多次生成發布發現發布包缺少了文件,那么請加上 MANIFEST.in 並將項目根目錄下的文件包含在內

** MANIFEST.in **

include requirements.txt

我在園子內找到一園友寫的一篇博文就是關於 MANIFEST.in 的,詳細參考 Python distribute到底使用package_data還是MANIFEST.in?

現在只要在pypi上注冊一個帳戶,然后回到項目的命令行狀態運行:

$ python setup.py sdist upload

就可以生成安裝包並直接上傳到pypi上了,接下來就可以用 pip install <你的項目名> 檢驗你的發布成果了。

Github

在進行持續集成之前更重要的部署當然是源碼控理了,關於 github 在此就不多說了,估計不會有人不知道它的大名的了。在發布到 Github 之前這里一份 .gitignore 文件可供參考,避免將無用的文件上傳到Github:

.gitignore

.idea
.webassets-cache
*.pyc
*.pypirc

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox
nosetests.xml

# SQLite databases
*.sqlite

# Virtual environment
venv

如果你在使用pyCharm 那么推薦安裝 .ignore 這個插件,可以直接支持多種的 ignore 文件。

自動構建

最后一公里就是就是自動構建,我們要達到的效果就是以后每次向 Github 提交變更時能自動執行部署和測試。如果條件允許可以使用Docker自建一台構建服務器完成這個過程。而另一個更佳做法是使用 Travis 的自動構建服務,只要用Github的賬號注冊,並將Reposiotry加入到Travis的跟蹤項后,當Github上的項目發生變更時Travis就會自動從Github上將最新版本的源代碼拉到一個獨立的Docker環境內直接進行部署和測試,每次測試結束還會向你的郵箱發送測試報告。如果在項目的Readme文件內引入自動更新的狀態標簽PyPI Shields/Pins就將發布與最終用戶之間的最后屏障打通。

Travis 可以支持很多的語言,文本以python為例,只要在項目的根目錄內加入.travis.yml的配置文件,配置Travis的自動構建行為(這是可選的,如果沒有此文件Travis 會執行默認構建)

以下是 .travis.yml 的完整內容:

language: python  #指定源碼的語言
python:           #指定python的版本
  - "2.7"
  - "pypy"

# 執行安裝前安裝必要的依賴環境
before_install:
  - sudo apt-get install node-less
  - sudo apt-get install coffeescript

# 執行安裝指令
install:
  - pip install -r tests/requirements.txt
  - python setup.py -q install

# 安裝成功后執行的指令集,此處為自動執行測試
script: python tests/test_suites.py

最后就是將狀態標簽加入到的Readme文件內,讓用戶即時了解當前源碼的狀態,效果如下圖:

要達到此效果只要在項目內加入readme.rst 文件並加以下以代碼:

將以下變量替換為您的項目注冊信息:

  • <github-username> - Github 用戶名
  • <repository> - Github 源碼項目名稱
  • <pypi-project-name> - 在Pypi上發布的可執行包名
項目名稱
=======

.. image:: https://secure.travis-ci.org/<github-username>/<repository>.png?branch=master
   :alt: Build Status
   :target: http://travis-ci.org/<github-username>/<repository>

.. image:: https://pypip.in/py_versions/<pypi-project-name>/badge.svg
    :target: https://pypi.python.org/pypi/<pypi-project-name/
    :alt: Supported Python versions

..  image:: https://pypip.in/status/<pypi-project-name/badge.svg
    :target: https://pypi.python.org/pypi/<pypi-project-name/
    :alt: Development Status

.. image:: https://pypip.in/version/<pypi-project-name/badge.svg
    :target: https://pypi.python.org/pypi/<pypi-project-name/
    :alt: Latest Version

.. image:: https://pypip.in/license/<pypi-project-name/badge.svg
    :target: https://pypi.python.org/pypi/<pypi-project-name/
    :alt: License

小結

自此整個項目的持續環境搭建宣告完成,以后每個版本的迭代就只是管理 pypi 上的可運行版本與github上的源碼版本即可。將這個方法延伸,則可應用於任何語言體驗下的項目開發。同樣只需要做的步驟:

  • 選定測試框架
  • 編寫各種測試
  • 將運行版本發布至公共包管理庫
  • 將源碼發布至源碼服務器 (github)
  • 接入構建服務器 (travis)

附:本文相關資源連接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM