約定大於配置:ApiTestEngine 實現熱載入機制

背景描述

在ApiTestEngine中編寫測試用例時,我們有時需要定義全局的變數,或者引用外部函數實現一些動態的計算邏輯。當前採用的方式是:

  • 若需定義全局的參數變數,則要在YAML/JSON的config中,使用variable_binds定義變數;
  • 若需引用外部函數,則要在YAML/JSON的config中,使用import_module_items導入指定的Python模塊。

雖然這種方式提供了極大的靈活性,但是對於用戶來說可能會顯得比較複雜。另外一方面,這種方式也會造成大量重複的情況。

例如,對於變數來說,假如我們的項目中存在100個測試場景,而每個場景中都需要將用戶賬號(test@ijd)作為全局變數來使用,那麼在現有模式下,我們只能在這100個YAML/JSON文件的config中都採用如下方式定義一遍:

- config:n name: "smoketest for scenario A."n variable_binds:n - username: test@ijdn

同樣的,對於外部函數來說,假如我們項目的100個測試場景都需要用到生成隨機字元串的函數(gen_random_string),那麼我們也不得不在這100個YAML/JSON文件的config中都導入一次該函數所在的Python模塊(假設相對於工作目錄的路徑為extra/utils.py)。

- config:n name: "smoketest for scenario A."n import_module_items:n - extra.utilsn

由此可見,當測試場景越來越多以後,要維護好全局變數和外部函數,必定會是一個很大的工作量。

那麼,如果既要能引用公共的變數和函數,又要減少重複的定義和導入,那要怎麼做呢?

pytest 的 conftest.py

前段時間在接觸pytest時,看到pytest支持conftest.py的插件機制,這是一種在測試文件中可以實現模塊自動發現和熱載入的機制。具體地,只要是在文件目錄存在命名為conftest.py的文件,裡面定義的hook函數都會在pytest運行過程中被導入,並可被測試用例進行調用。同時,conftest.py存在優先順序策略,從測試用例所在目錄到系統根目錄的整個路徑中,越靠近測試用例的conftest.py優先順序越高。

其實這也是採用了約定大於配置(convention over configuration)的思想。約定大於配置是一種軟體設計範式,旨在減少軟體開發人員需做決定的數量,在遵從約定的過程中就不自覺地沿用了最佳工程實踐。我個人也是比較喜歡這種方式的,所以在設計ApiTestEngine的時候,也借鑒了一些類似的思想。

受到該啟發,我想也可以採用類似的思想,採用自動熱載入的機制,解決背景描述中存在的重複定義和引用的問題。

既然是約定大於配置,那麼我們首先就得定一個默認的Python模塊名,類似於pytest的conftest.py。

這就是debugtalk.py。

debugtalk.py 的命名由來

為啥會採用debugtalk.py這個命名呢?

其實當時在想這個名字的時候也是耗費了很多心思,畢竟是要遵從約定大於配置的思想,因此在設計這個約定的命名時就格外謹慎,但始終沒有想到一個既合適又滿意的。

在我看來,這個命名應該至少滿足如下兩個條件:

  • 唯一性強
  • 簡單易記

首先,約定的模塊名應該具有較強的唯一性和較高的區分度,是用戶通常都不會採用的命名;否則,可能就會出現測試用例在運行過程中,熱載入時導入預期之外的Python模塊。

但也不能僅僅為了具有區分度,就使用一個很長或者毫無意義的字元串作為模塊名;畢竟還是要給用戶使用的,總不能每次寫用例時還要去查看下文檔吧;所以命名簡單易記便於用戶使用也很重要。

也是因為這兩個有點互相矛盾的原則,讓我在設計命名時很是糾結。最終在拉同事討論良久而無果的時候,同事說,不如就命名為debugtalk.py得了。

仔細一想,這命名還真符合要求。在唯一性方面,採用debugtalk.py在Google、Bing、Baidu等搜索引擎中採用精確匹配,基本沒有無關信息,這樣在後續遇到問題時,也容易搜索到已有的解決方案;而在簡單易記方面,相信這個命名也不會太複雜。

當然,debugtalk.py只是作為框架默認載入的Python模塊名,如果你不喜歡,也可以進行配置修改。

熱載入機制實現原理

然後,再來講解下熱載入機制的實現。

其實原理也不複雜,從背景描述可以看出,我們期望實現的需求主要有兩點:

  • 自動發現debugtalk.py函數模塊,並且具有優先順序策略;
  • 將debugtalk.py函數模塊中的變數和函數導入到當前框架運行的內存空間。

將這兩點與測試用例引擎的實現機制結合起來,ApiTestEngine在運行過程中的熱載入機制應該就如下圖所示。

這個流程圖對熱載入機制描述得已經足夠清晰了,我再針對其中的幾個點進行說明:

1、在初始化測試用例集(testset)的時候,除了將config中variable_binds和import_module_items指定的變數和函數導入外,還會默認導入ate/built_in.py模塊。之所以這麼做,是因為對於大多數系統可能都會用到一些通用的函數,例如獲取當前時間戳(get_timestamp)、生成隨機字元串(gen_random_string)等。與其在每個項目中都單獨去實現這些函數,不如就將其添加到框架中作為默認支持的函數(相當於框架層面的debugtalk.py),這樣大家在項目中就不需要再重複做這些基礎性工作了。

2、在ApiTestEngine框架中,存在測試用例(testcase)和測試用例集(testset)兩個層面的作用域,兩者的界限十分明確。這樣設計的目的在於,我們既可以實現用例集層面的變數和函數的定義和導入,也可以保障各個用例之間的獨立性,不至於出現作用域相互污染的情況。具體地,作用域在用例集初始化時定義或導入的變數和函數,會存儲在用例集層面的作用域;而在運行每條測試用例時,會先繼承(deepcopy)用例集層面的作用域,如果存在同名的變數或函數定義,則會對用例集層面的變數和函數進行覆蓋,同時用例集層面的變數和函數也並不會被修改。

3、從熱載入的順序可以看出,查找變數或函數的順序是從測試用例所在目錄開始,沿著父路徑逐層往上,直到系統的根目錄。因此,我們可以利用這個優先順序原則來組織我們的用例和依賴的Python函數模塊。例如,我們可以將不同模塊的測試用例集文件放在不同的文件夾下:針對各個模塊獨有的依賴函數和變數,可以放置在對應文件夾的debugtalk.py文件中;而整個項目公共的函數和變數,就可以放置到項目文件夾的debugtalk.py中。

文件組織結構如下所示:

? project ? tree .n.n├── debugtalk.pyn├── module_An│ ├── __init__.pyn│ ├── debugtalk.pyn│ ├── testsetA1.ymln│ └── testsetA2.ymln└── module_Bn ├── __init__.pyn ├── debugtalk.pyn ├── testsetB1.ymln └── testsetB2.ymln

這其中還有一點需要格外注意。因為我們在框架運行過程中需要將debugtalk.py作為函數模塊進行導入,因此我們首先要保障debugtalk.py滿足Python模塊的要求,也就是在對應的文件夾中要包含__init__.py文件。

如果對熱載入機制的實現感興趣,可直接閱讀框架源碼,重點只需查看ate/utils.py中的三個函數:

  • search_conf_item(start_path, item_type, item_name)
  • get_imported_module_from_file(file_path)
  • filter_module(module, filter_type)

測試用例編寫方式的變化

在新增熱載入機制之後,編寫測試用例的方式發生一些改變(優化),主要包括三點:

  • 導入Python模塊的關鍵詞改名為import_module_items(原名為import_module_functions);
  • 不再需要顯式指定導入的Python模塊路徑,變更為熱載入機制自動發現;
  • Python模塊中的變數也會被導入,公共變數可放置在Python模塊中,而不再必須通過variable_binds定義。

考慮到兼容性問題,框架升級的同時也保留了對原有測試用例編寫方式的支持,因此框架升級對已有測試用例的正常運行也不會造成影響。不過,我還是強烈建議大家採用最新的用例編寫方式,充分利用熱載入機制帶來的便利。

寫在最後

現在回過頭來看ApiTestEngine的演進歷程,以及之前寫的關於ApiTestEngine設計方面的文章,會發現當初的確是有一些考慮不周全的地方。也許這也是編程的樂趣所在吧,在前行的道路中,總會有新的感悟和新的收穫,迭代優化的過程,就彷彿是在打磨一件藝術品。

這種感覺,甚好!


推薦閱讀:

Python的在線學習地址?
如何評價 Quora 的 Ultralisk 並行架構?
如何用python 寫一個爬蟲用來爬P 站特定標籤下的高贊作品?
想要用 python 做爬蟲, 是使用 scrapy框架還是用 requests, bs4 等庫?
使用了Gunicorn或者uWSGI,為什麼還需要Nginx?

TAG:Python | 插件 | 开源项目 |