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測試有幾個十分實用的工具
關於 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
執行成功後將會自動產生 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 的標準專案結構,要將 static
和 templates
的內容包含於釋出包內,那麼:
#...
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上註冊一個帳戶,然後回到專案的命令列狀態執行:
將專案註冊到 pypi 上
$ python setup.py register
生成安裝包將上傳至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)
附:本文相關資源連線
相關文章
- TDD 實踐-FizzFuzzWhizz(二)
- TDD 實踐-FizzFuzzWhizz(一)
- TDD 實踐-FizzFuzzWhizz(三)
- 基於 GitLab CI 的前端工程CI/CD實踐Gitlab前端
- TDD及單元測試最佳實踐
- Redis 在 Web 專案中的應用與實踐RedisWeb
- Redis在Web專案中的應用與實踐RedisWeb
- 《探索Python Requests中的代理應用與實踐》Python
- Elasticsearch在Laravel中的實踐ElasticsearchLaravel
- 基於OpenStack+Docker設計與實現CI/CD——基於Docker技術的CI&CD實踐Docker
- COMPASS專案CI實踐
- CI Weekly #6 | 再談 Docker / CI / CD 實踐經驗Docker
- 深度學習在搜尋業務中的探索與實踐深度學習
- Presto在滴滴的探索與實踐REST
- axios在vue中的實踐iOSVue
- Proxyless Mesh 在 Dubbo 中的實踐
- 協程在RN中的實踐
- Immutable 操作在 React 中的實踐React
- CI/CD 持續整合部署實踐
- 大資料在快狗叫車中的應用與實踐大資料
- 關於Python中math 和 decimal 模組的解析與實踐PythonDecimal
- CI Weekly #3 | 關於微服務、Docker 實踐與 DevOps 指南微服務Dockerdev
- ConstraintLayout在專案中實踐與總結AI
- 開源實踐 | 攜程在OceanBase的探索與實踐
- 開源實踐 | 攜程在 OceanBase 的探索與實踐
- MongoDB 在評論中臺的實踐MongoDB
- TypeScript在react專案中的實踐TypeScriptReact
- TypeScript在node專案中的實踐TypeScript
- 策略模式在應用中的實踐模式
- Geospatial Data 在 Nebula Graph 中的實踐
- 人肉工程在機器學習實踐中的作用機器學習
- python BDD&TDDPython
- 深度學習在美團配送ETA預估中的探索與實踐深度學習
- 文件驅動開發模式在 AIMS 中的應用與實踐模式AI
- 【PWA學習與實踐】(6) 在Chrome中除錯你的PWAChrome除錯
- 服務計算 TDD實踐——實現快速排序演算法排序演算法
- Artifactory & GitLab CI持續整合實踐Gitlab
- Elasticsearch 7.2 在 Laravel 中實踐ElasticsearchLaravel