1. 程式人生 > >優化自動化測試流程,使用 flask 開發一個 toy jenkins工具

優化自動化測試流程,使用 flask 開發一個 toy jenkins工具

 

1、自動化

 

某一天你入職了一家高大上的科技公司,開心的做著軟體測試的工作,每天點點點,下班就走,晚上陪女朋友玩王者,生活很愜意。

 

但是美好時光一般不長,這種生活很快被女主管打破。為了提升公司測試效率,公司決定引入自動化流程,你在網上搜了一套技術方案 python + selenium,迅速寫了一套自動化測試的指令碼。

 

from selenium import webdriver


def test_selenium():
    driver = webdriver.Firefox()
    driver.get("http://www.baidu.com")
    ...
    driver.quit()
...

 

編寫指令碼的日子很累,你需要每天加班,而且沒有加班工資。 雖然如此,你也沒有太多怨言,因為你能明顯感覺到自己一點點掌握了自動化測試的流程,正在踏入職業發展的新階段。這套指令碼很快用於公司的主流程測試,也會在迴歸測試中使用。

 

因為大量的重複勞動都可以用這套自動化測試指令碼代替,於是你又有時間陪女朋友了,上班也可以偶爾划水了,也可以時不時瞄一瞄自己的基金有沒有漲。

 

當然,美好時光一般不長。在一次大改版中,前端頁面發生了大量變化,你的自動化測試程式碼因為沒有做抽象封裝,基本已經不能用了。

 

又可以加班了,生活又可以充實起來了。你動用了一些像 PageObject 的模式對程式碼進行了重新設計,也加入了關鍵字驅動,儘量讓測試邏輯變成可配置的。 設計完成以後,當前端頁面變化時,只需要重點維護關鍵字表格。

 

你又為公司做了一些貢獻,你已經完全勝任自動化測試的工作,甚至能夠帶一兩個小弟。他們時不時找你問一些問題,但是對於自動化的維護工作還是要靠你自己,當你請假時,這些工作只能停滯。於是公司希望你做一些改進,讓功能測試人員也可以執行這些自動化測試。

 

 

2、開始測試平臺

 

你看到網上有很多人提到測試平臺,想著自己也可以做一個視覺化平臺,這樣功能測試人員也可以通過在介面上進行簡單的設定,就可以使用底層的自動化程式碼了。很快 flask 出現在你的視線中,你做的第一個功能就是實現類似於 jenkins 的構建功能。

 

首先,你搭建了一個 flask 服務,服務啟動後,你能順利訪問 5000 埠。

from flask import Flask
app = Flask(__name__)
app.run(port=5000)

 

然後,你配置了一個 url 地址,當訪問這個 url 地址時,服務會呼叫一個函式,這個 url 和函式的繫結關係就是路由。函式的返回值可以是普通字串,可以是 json 資料,也可以是 html 頁面。

 

@app.route('/')
def index():
    "show all projects in workspace dir"
    workspace = pathlib.Path(app.root_path) / 'workspace'
    projects = [project.name for project in workspace.iterdir()]
    return render_template('index.html', projects=projects)

 

 

 

上面的程式碼就是模仿 jenkins, 把自動化測試的指令碼放在專案的 workspace 目錄下,當訪問 / 根路徑時,index 函式就會被呼叫。index 函式的作用就是列舉 workspace 目錄下的所有專案名,通過 return 展示在前端介面。具體的前端程式碼如下:

 

<h2>展示所有的專案</h2>
{% for p in projects %}
  <div>
    {{ p }} 
    <a href="/build?project={{p}}">構建</a>
  </div>
{% endfor %}

 

 

 

在頁面上點選構建,程式會跳轉到 flask 設定好的 /build 這個 url 中,這個路由負責執行自動化測試的程式碼,他會接收使用者傳過來的 project 引數,找到在 workspace 目錄下的專案,再執行自動化測試指令(這裡統一用 pytest 指令)。

@app.route("/build", methods=['get', 'post'])
def build():
    project_name = request.args.get('project')
    pytest.main([f'workspace/{project_name}'])
    return "build success"

 

到目前為止,完整的流程是這樣的:首先,在平臺首頁會展示所有可以構建的專案,這些專案其實就是把 workspace 子目錄當中的目錄名列舉出來;然後,點選專案旁邊的構建按鈕,跳轉到 /build,根據專案名稱執行自動化指令,等待自動化任務執行完成,返回 build success。

 

 

3、優化

 

你基本上已經實現了功能,現在功能測試人員可以通過你搭建的簡易平臺執行自動化命令。但是這個平臺還存在一些問題:第一、沒有收集到構建資訊,無法檢視測試之後的結果;第二、使用者必須等待自行測試指令碼執行完成,才能返回前端具體的結果,如果自動化測試的執行時間很長,使用者會一直停在這個頁面,無法做其他事情。

 

你想到了併發程式設計,建立一個子程序單獨去執行自動化測試指令碼,因為子程序可以和主程序獨立,所以不需要等待子程序執行完成,主程序就可以立即給前端返回結果。於是你重新編寫了 build 函式:

 

@app.route("/build")
def build():
    id = uuid.uuid4().hex
    project_name = request.args.get('project')
    with open(id, mode='w', encoding='utf-8') as f:
        subprocess.Popen(
            ['pytest', f'workspace/{project_name}'],
            stdout=f
        )
    return redirect(f'/build-history/{id}')

 

1、首先,通過 uuid 生成一個 id 號來表示這一次構建任務,之後可以通過這個 id 號檢視此次構建的記錄;

2、通過 subprocess 建立子程序執行自動化任務,把輸出結果儲存到檔案當中,檔名就是生成的 id 號,之後想檢視構建的結果時,只需要讀取這個檔案當中的內容;

3、只要子程序建立成功,馬上通過 redirect 重定向到檢視結果的 url, 此時並不需要等到子程序執行完就可以檢視構建結果。

 

檢視構建結果只需要通過 id 讀取檔案中的內容返回。

@app.route("/build-history/<id>")
def build_history(id):
    with open(id, encoding='utf-8') as f:
        data = f.read()
    return data

 

 

4、生成器

上面讀取檔案的程式碼有點問題。當構建重定向到 /build-histrory 後,此時自動化測試指令碼才剛剛執行,讀取檔案中的內容是空的。只有當測試指令碼執行,產生越來越多的執行記錄,檔案中才會出現更多的內容,你必須手動重新整理頁面才能獲取這些新內容。 當自動化任務執行時間很長的時候,你需要不停的重新整理 /build-history 頁面才能獲取最新的構建資訊。直到子程序結束,不再有新的內容被寫入檔案。

 

為了動態獲取檔案資料,你使用了生成器惰性獲取資料,在 /build-history 的頁面載入過程中,只要執行自動化任務的子程序還在執行,就不停的讀取檔案內容,將它們動態的返回給前端頁面。

 

為了判斷子程序的狀態,在 /build 的時候,把子程序的 pid 傳給 /build-history。

@app.route("/build", methods=['get', 'post'])
def build():
    id = uuid.uuid4().hex
    project_name = request.args.get('project')
    with open(id, mode='w', encoding='utf-8') as f:
        proc = subprocess.Popen(
            ['pytest', f'workspace/{project_name}'],
            stdout=f
        )
    return redirect(f'/build-history/{id}?pid={proc.pid}')

 

在檢視結果時,先編寫一個生成器 stream, 每次讀取檔案中 100 長度的資料,直到程序執行結束。除了通過構建後的重定向,你也可以手動輸入 id,檢視歷史構建記錄。此時只需傳 id, 不需要傳程序名,直接讀取檔案中的資料。就算檔案特別大,也可以通過批量載入,不至於因為同時讀取大量資料給伺服器造成壓力。

import psutil


@app.route("/build-history/<id>")
def build_history(id):
    pid = request.args.get('pid', None)
    def stream():
        f = open(id, encoding='utf-8')
        if not pid:
            while True:
                data = f.read(100)
                if not data:
                    break
                yield data
        else:
            try:
                proc = psutil.Process(pid=int(pid))
            except:
                return 'no such pid'
            else:
                while proc.is_running():
                    data = f.read(100)
                    yield data
    return Response(stream())

 

最後效果:

 

5、總結

 

一般來說,做自動化測試只需要做到第一步,有指令碼可以執行,就可以代替重複勞動。做測試平臺只是讓指令碼變得更加好用。

 

但是有很多的測試平臺讓自動化執行起來更加複雜,要配置很多很多引數才能跑一個完整的測試用例,這似乎有點折本求末,也是很多人都在做的事情。

 

本文通過 flask 程式實現了一個最簡單的 toy jenkins,輔助理解像 jenkins 這樣的工具如何執行任務。其實像簡單的構建任務,做起來也有很多問題需要解決,這些只有在遇到具體業務的時候我們才會去思考。

 

希望我們做的工具都是實用的,好用