欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺談基于Pytest框架的自動化測試開發(fā)實踐

 更新時間:2021年12月23日 09:59:37   作者:liuchunming033  
Pytest是Python的一種易用、高效和靈活的單元測試框架,本文主要介紹了基于Pytest框架的自動化測試開發(fā)實踐,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下

Pytest是Python的一種易用、高效和靈活的單元測試框架,可以支持單元測試和功能測試。本文不以介紹Pytest工具本身為目的,而是以一個實際的API測試項目為例,將Pytest的功能應用到實際的測試工程實踐中,教大家將Pytest用起來。

在開始本文之前,我想跟大家澄清兩個概念,一個是測試框架一個是測試工具。很多人容易把他們搞混了,測試框架是諸如Unittest、Pytest、TestNG這類,而測試工具指的則是Selenium、Appium、Jmeter這類。

測試框架的作用是,幫助我們管理測試用例、執(zhí)行測試用例、參數(shù)化、斷言、生成測試報告等基礎性工作,讓我們將精力用在測試用例的編寫上。好的測試框架應該具有很高的擴展性,支持二次開發(fā),并能夠支持多種類型的自動化測試。

測試工具的作用是為了完成某一類型的測試,比如Selenium用于對WEB UI進行自動化測試,Appium用來對APP進行自動化測試,Jmeter可以用來進行API自動化測試和性能測試。另外,Java語言中OkHttp庫,Python語言中的requests庫,這些HTTP的client也可以看做是一種API測試工具。

澄清了這兩個概念,說一下本文的目的。其實網(wǎng)上已經(jīng)有很多教程,包括官方文檔,都是以介紹Pytest的功能為出發(fā)點,羅列了各種功能的使用方法,大家看完之后會感覺都明白了,但是還是不知道如何與實際項目相結合,真正落地用起來。本文不以介紹Pytest工具本身為目的,而是以一個實際的API測試項目為例,通過單元測試框架Pytest和Python的Requests庫相結合,將Pytest功能應用到實際的測試工程實踐中,教大家將Pytest用起來。

請相信我,使用Pytest會讓你的測試工作非常高效。

01 — Pytest核心功能

在開始使用Pytest之前,先來了解一下Pytest的核心功能,根據(jù)官方網(wǎng)站介紹,它具有如下功能和特點:

  • 非常容易上手,入門簡單,文檔豐富,文檔中有很多實例可以參考。
  • 能夠支持簡單的單元測試和復雜的功能測試。
  • 支持參數(shù)化。
  • 能夠執(zhí)行全部測試用例,也可以挑選部分測試用例執(zhí)行,并能重復執(zhí)行失敗的用例。
  • 支持并發(fā)執(zhí)行,還能運行由nose, unittest編寫的測試用例。
  • 方便、簡單的斷言方式。
  • 能夠生成標準的Junit XML格式的測試結果。
  • 具有很多第三方插件,并且可以自定義擴展。
  • 方便的和持續(xù)集成工具集成。

Pytest的安裝方法與安裝其他的python軟件無異,直接使用pip安裝即可。

$ pip install -U pytest

安裝完成后,可以通過下面方式驗證是否安裝成功:

$ py.test --help

如果能夠輸出幫助信息,則表示安裝成功了。

接下來,通過開發(fā)一個API自動化測試項目,詳細介紹以上這些功能是如何使用的。

02 — 創(chuàng)建測試項目

先創(chuàng)建一個測試項目目錄api_pytest,為這個項目創(chuàng)建虛擬環(huán)境。關于虛擬環(huán)境的創(chuàng)建,可以參考這篇文章《利用pyenv和pipenv管理多個相互獨立的Python虛擬開發(fā)環(huán)境》。這里我們直接介紹如何使用,執(zhí)行下面兩條命令:

$ mkdir api_pytest
$ pipenv --python 3.7.7

這樣,項目目錄和虛擬環(huán)境就創(chuàng)建完成了。

接著,安裝依賴包,第一個是要安裝pytest,另外本文是以API自動化測試為例,因此還要安裝一下HTTP 的client包requests。

$ pipenv install pytest requests

現(xiàn)在我們創(chuàng)建一個data目錄,用來存放測試數(shù)據(jù),一個tests目錄,用來存放測試腳本,一個config目錄,用來存放配置文件,一個utils目錄從來存放工具。

$ mkdir data
$ mkdir tests
$ mkdir config
$ mkdir utils

現(xiàn)在,項目的目錄結構應該是如下這樣:

$ tree
.
├── Pipfile
├── Pipfile.lock
├── config
├── data
├── tests
└── utils
?
4 directories, 2 files

至此測試項目就創(chuàng)建完成了。接著編寫測試用例。

03 — 編寫測試用例

在這部分,我們以測試豆瓣電影列表API和電影詳情API為例,編寫測試用例。

這兩個API信息如下:

接口 示例
電影列表 http://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a&start=0&count=10
電影詳情 https://api.douban.com/v2/movie/subject/30261964?apikey=0df993c66c0c636e29ecbb5344252a4a

我們先寫電影列表API的自動化測試用例,設置3個校驗點:

  • 驗證請求中的start與響應中的start一致。
  • 驗證請求中的count與響應中的count一致。
  • 驗證響應中的title是"正在上映的電影-上海"。

在tests目錄里面,創(chuàng)建個test_in_theaters.py文件,里面編寫測試用例,內(nèi)容如下:

import requests

class TestInTheaters(object):
    def test_in_theaters(self):
        host = "http://api.douban.com"
        path = "/v2/movie/in_theaters"
        params = {"apikey": "0df993c66c0c636e29ecbb5344252a4a",
                  "start": 0,
                  "count": 10
                  }
        headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
        }
        r = requests.request("GET", url=host + path, headers=headers, params=params)
        response = r.json()
        assert response["count"] == params["count"]
        assert response["start"] == params["start"]
        assert response["title"] == "正在上映的電影-上海", "實際的標題是:{}".format(response["title"])

你可能會問,這就是測試用例了?這就是基于Pytest的測試用例了嗎?答案是肯定的。基于Pytest編寫自動化測試用例,與編寫平常的Python代碼沒有任何區(qū)別,唯一的區(qū)別在于文件名、函數(shù)名或者方法名要以test_開頭或者_test結尾,類名以Test開頭。

Pytest會在test_*.py 或者 *_test.py 文件中,尋找class外邊的test_開頭的函數(shù),或者Test開頭的class里面的test_開頭的方法,將這些函數(shù)和方法作為測試用例來管理??梢酝ㄟ^下面的命令,查看Pytest收集到哪些測試用例:

$ py.test --collect-only
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest
collected 1 item                                                                                                                 
<Module tests/test_in_theaters.py>
  <Class TestInTheaters>
      <Function test_in_theaters>
?
===================================================== no tests ran in 0.10s ======================================================

從結果中看到,一共有一條測試用例,測試用例位于tests/test_in_theaters.py這個module里面TestInTheaters這個類中的test_in_theaters這個方法。

在Pytest中斷言使用的是Python自帶的assert語句,非常簡單。

04 — 執(zhí)行測試用例

下面來運行這個測試:

$ py.test tests/
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest
collected 1 item
?
tests/test_in_theaters.py .                                                                                                [100%]
?
======================================================= 1 passed in 0.61s ========================================================
(api_pytest) MBC02X21W4G8WN:api_pytest chunming.liu$ py.test tests/
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest
collected 1 item
?
tests/test_in_theaters.py F                                                                                                [100%]
?
============================================================ FAILURES ============================================================
________________________________________________ TestInTheaters.test_in_theaters _________________________________________________
?
self = <test_in_theaters.TestInTheaters object at 0x110eee9d0>
?
    def test_in_theaters(self):
        host = "http://api.douban.com"
        path = "/v2/movie/in_theaters"
        params = {"apikey": "0df993c66c0c636e29ecbb5344252a4a",
                  "start": 0,
                  "count": 10
                  }
        headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
        }
        r = requests.request("GET", url=host + path, headers=headers, params=params)
        response = r.json()
        assert response["count"] == params["count"]
        assert response["start"] == params["start"]
        assert response["total"] == len(response["subjects"])
?
>       assert response["title"] == "正在上映的電影-上海", "實際的標題是:{}".format(response["title"])
E       AssertionError: 實際的標題是:正在上映的電影-北京
E       assert '正在上映的電影-北京' == '正在上映的電影-上海'
E         - 正在上映的電影-上海
E         ?         ^^
E         + 正在上映的電影-北京
E         ?         ^^
?
tests/test_in_theaters.py:20: AssertionError
==================================================== short test summary info =====================================================
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters - AssertionError: 實際的標題是正在上映的電影-北京
======================================================= 1 failed in 0.96s ========================================================

這個命令執(zhí)行時,會在tests/目錄里面尋找測試用例。執(zhí)行測試的時候,如果不指定測試用例所在目錄,Pytest會在當前的目錄下,按照前面介紹的規(guī)則尋找測試用例并執(zhí)行。

通過上面的測試輸出,我們可以看到該測試過程中,一共收集到了一個測試用例,測試結果是失敗的(標記為F),并且在FAILURES部分輸出了詳細的錯誤信息,通過這些信息,我們可以分析測試失敗的原因。上面測試用例的失敗原因是在斷言title的時候出錯了,預期的title是“正在上映的電影-上?!?,但是實際是“正在上映的電影-北京”,預期和實際的對比非常直觀。

執(zhí)行測試用例的方法還有很多種,都是在py.test后面添加不同的參數(shù)即可,我在下面羅列了一下:

$ py.test               # run all tests below current dir
$ py.test test_module.py   # run tests in module
$ py.test somepath      # run all tests below somepath
$ py.test -k stringexpr # only run tests with names that match the
                      # the "string expression", e.g. "MyClass and not method"
                      # will select TestMyClass.test_something
                      # but not TestMyClass.test_method_simple
$ py.test test_module.py::test_func # only run tests that match the "node ID",
                                    # e.g "test_mod.py::test_func" will select
                                    # only test_func in test_mod.py

上面這些用法,通過注釋很容易理解。在測試執(zhí)行過程中,這些方法都有機會被用到,最好掌握一下。

05 — 數(shù)據(jù)與腳本分離

03小節(jié)的測試用例,將測試數(shù)據(jù)和測試代碼放到了同一個py文件中,而且是同一個測試方法中,產(chǎn)生了緊耦合,會導致修改測試數(shù)據(jù)或測試代碼時,可能會相互影響,不利于測試數(shù)據(jù)和測試腳本的維護。比如,為測試用例添加幾組新的測試數(shù)據(jù),除了準備測試數(shù)據(jù)外,還要修改測試代碼,降低了測試代碼的可維護性。

另外接口測試往往是數(shù)據(jù)驅動的測試,測試數(shù)據(jù)和測試代碼放到一起也不方便借助Pytest做參數(shù)化。

將測試代碼和測試數(shù)據(jù)分離已經(jīng)是測試領域中的共識了。在data/目錄下創(chuàng)建一個用于存放測試數(shù)據(jù)的Yaml文件test_in_theaters.yaml,內(nèi)容如下:

---
tests:
- case: 驗證響應中start和count與請求中的參數(shù)一致
  http:
    method: GET
    path: /v2/movie/in_theaters
    headers:
      User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
    params:
      apikey: 0df993c66c0c636e29ecbb5344252a4a
      start: 0
      count: 10
  expected:
    response:
      title: 正在上映的電影-上海
      count: 10
      start: 0

熟悉Yaml格式的同學,應該很容易看懂上面測試數(shù)據(jù)文件的內(nèi)容。這個測試數(shù)據(jù)文件中,有一個數(shù)組tests,里面包含的是一條完整的測試數(shù)據(jù)。一個完整的測試數(shù)據(jù)由三部分組成:

  • case,表示測試用例名稱。
  • http,表示請求對象。
  • expected,表示預期結果。

http這個請求對象包含了被測接口的所有參數(shù),包括請求方法、請求路徑、請求頭、請求參數(shù)。
expected表示預期結果,上面的測試數(shù)據(jù)中,只列出了對請求響應的預期值,實際測試中,還可以列出對數(shù)據(jù)庫的預期值。

測試腳本也要做相應的改造,需要讀取test_in_theaters.yaml文件獲取請求數(shù)據(jù)和預期結果,然后通過requests發(fā)出請求。修改后的測試代碼如下:

import requests
import yaml
?
?
def get_test_data(test_data_path):
    case = []  # 存儲測試用例名稱
    http = []  # 存儲請求對象
    expected = []  # 存儲預期結果
    with open(test_data_path) as f:
        dat = yaml.load(f.read(), Loader=yaml.SafeLoader)
        test = dat['tests']
        for td in test:
            case.append(td.get('case', ''))
            http.append(td.get('http', {}))
            expected.append(td.get('expected', {}))
    parameters = zip(case, http, expected)
    return case, parameters
?
?
cases, parameters = get_test_data("/Users/chunming.liu/learn/api_pytest/data/test_in_theaters.yaml")
list_params=list(parameters)
?
class TestInTheaters(object):
    def test_in_theaters(self):
        host = "http://api.douban.com"
        r = requests.request(list_params[0][1]["method"],
                             url=host + list_params[0][1]["path"],
                             headers=list_params[0][1]["headers"],
                             params=list_params[0][1]["params"])
        response = r.json()
        assert response["count"] == list_params[0][2]['response']["count"]
        assert response["start"] == list_params[0][2]['response']["start"]
        assert response["total"] == len(response["subjects"])
        assert response["title"] == list_params[0][2]['response']["title"], "實際的標題是:{}".format(response["title"])

注意,讀取Yaml文件,需要安裝PyYAML包。

測試腳本中定義了一個讀取測試數(shù)據(jù)的函數(shù)get_test_data,通過這個函數(shù)從測試數(shù)據(jù)文件test_in_theaters.yaml中讀取到了測試用例名稱case,請求對象http和預期結果expected。這三部分分別是一個列表,通過zip將他們壓縮到一起。

測試方法test_in_theaters并沒有太大變化,只是發(fā)送請求所使用的測試數(shù)據(jù)不是寫死的,而是來自于測試數(shù)據(jù)文件了。

通常情況下,讀取測試數(shù)據(jù)的函數(shù)不會定義在測試用例文件中,而是會放到utils包中,比如放到utils/commonlib.py中。至此,整個項目的目錄結構應該是如下所示:

$ tree
.
├── Pipfile
├── Pipfile.lock
├── config
├── data
│   └── test_in_theaters.yaml
├── tests
│   └── test_in_theaters.py
└── utils
    └── commlib.py

這樣,我們修改測試腳本,就修改test_in_theaters.py,變更測試數(shù)據(jù),就修改test_in_theaters.yaml。但是目前看,感覺好像并沒有真正看到測試數(shù)據(jù)和腳本分離的厲害之處,或者更加有價值的地方,那么我們接著往下看。

06 — 參數(shù)化

上面我們將測試數(shù)據(jù)和測試腳本相分離,如果要為測試用例添加更多的測試數(shù)據(jù),往tests數(shù)組中添加更多的同樣格式的測試數(shù)據(jù)即可。這個過程叫作參數(shù)化。

參數(shù)化的意思是對同一個接口,使用多種不同的輸入對其進行測試,以驗證是否每一組輸入?yún)?shù)都能得到預期結果。Pytest提供了pytest.mark.paramtrize這種方式來進行參數(shù)化,我們先看下官方網(wǎng)站提供的介紹pytest.mark.paramtrize用法的例子:

# content of tests/test_time.py
import pytest
?
from datetime import datetime, timedelta
?
testdata = [
    (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
    (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]
?
?
@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
    diff = a - b
    assert diff == expected

執(zhí)行上面的腳本將會得到下面的輸出,測試方法test_timedistance_v0被執(zhí)行了兩遍,第一遍執(zhí)行用的測試數(shù)據(jù)是testdata列表中的第一個元組,第二遍執(zhí)行時用的測試數(shù)據(jù)是testdata列表中的第二個元組。這就是參數(shù)化的效果,同一個腳本可以使用不同的輸入?yún)?shù)執(zhí)行測試。

============================= test session starts ==============================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /Users/chunming.liu/.local/share/virtualenvs/api_pytest-wCozfXSU/bin/python
cachedir: .pytest_cache
rootdir: /Users/chunming.liu/learn/api_pytest/tests
collecting ... collected 2 items
?
test_time.py::test_timedistance_v0[a0-b0-expected0] PASSED    [ 50%]
test_time.py::test_timedistance_v0[a1-b1-expected1] PASSED    [100%]
?
============================== 2 passed in 0.02s ===============================

照貓畫虎,對我們自己的測試項目中的測試腳本進行如下修改。

import pytest
import requests
?
from utils.commlib import get_test_data
?
cases, list_params = get_test_data("/Users/chunming.liu/learn/api_pytest/data/test_in_theaters.yaml")
?
?
class TestInTheaters(object):
    @pytest.mark.parametrize("case,http,expected", list(list_params), ids=cases)
    def test_in_theaters(self, case, http, expected):
        host = "http://api.douban.com"
        r = requests.request(http["method"],
                             url=host + http["path"],
                             headers=http["headers"],
                             params=http["params"])
        response = r.json()
        assert response["count"] == expected['response']["count"]
        assert response["start"] == expected['response']["start"]
        assert response["title"] == expected['response']["title"], "實際的標題是:{}".format(response["title"])

在測試方法上面添加了一個裝飾器@pytest.mark.parametrize,裝飾器會自動對list(list_params)解包并賦值給裝飾器的第一參數(shù)。裝飾器的第一個參數(shù)中逗號分隔的變量可以作為測試方法的參數(shù),在測試方法內(nèi)就可以直接獲取這些變量的值,利用這些值發(fā)起請求和進行斷言。裝飾器還有一個參數(shù)叫ids,這個值作為測試用例的名稱將打印到測試結果中。

在執(zhí)行修改后的測試腳本前,我們在測試數(shù)據(jù)文件再增加一組測試數(shù)據(jù),現(xiàn)在測試數(shù)據(jù)文件中,包含了兩組測試數(shù)據(jù):

---
tests:
- case: 驗證響應中start和count與請求中的參數(shù)一致
  http:
    method: GET
    path: /v2/movie/in_theaters
    headers:
      User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
    params:
      apikey: 0df993c66c0c636e29ecbb5344252a4a
      start: 0
      count: 10
  expected:
    response:
      title: 正在上映的電影-上海
      count: 10
      start: 0
- case: 驗證響應中title是"正在上映的電影-北京"
  http:
    method: GET
    path: /v2/movie/in_theaters
    headers:
      User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
    params:
      apikey: 0df993c66c0c636e29ecbb5344252a4a
      start: 1
      count: 5
  expected:
    response:
      title: 正在上映的電影-北京
      count: 5
      start: 1

現(xiàn)在我們執(zhí)行一下測試腳本,看看效果:

$ export PYTHONPATH=/Users/chunming.liu/learn/api_pytest
$ py.test tests/test_in_theaters.py 
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
collected 2 items                                                                                                                
?
tests/test_in_theaters.py F.                                                                                               [100%]
?
============================================================ FAILURES ============================================================
___________________________________ TestInTheaters.test_in_theaters[驗證響應中start和count與請求中的參數(shù)一致] ___________________________________


?
self = <test_in_theaters.TestInTheaters object at 0x102659510>, case = '驗證響應中start和count與請求中的參數(shù)一致'
http = {'headers': {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chr...T', 'params': {'apikey': '0df993c66c0c636e29ecbb5344252a4a', 'count': 10, 'start': 0}, 'path': '/v2/movie/in_theaters'}
expected = {'response': {'count': 10, 'start': 0, 'title': '正在上映的電影-上海'}}
?
    @pytest.mark.parametrize("case,http,expected", list(list_params), ids=cases)
    def test_in_theaters(self, case, http, expected):
        host = "http://api.douban.com"
        r = requests.request(http["method"],
                             url=host + http["path"],
                             headers=http["headers"],
                             params=http["params"])
        response = r.json()
        assert response["count"] == expected['response']["count"]
        assert response["start"] == expected['response']["start"]
>       assert response["title"] == expected['response']["title"], "實際的標題是:{}".format(response["title"])
E       AssertionError: 實際的標題是:正在上映的電影-北京
E       assert '正在上映的電影-北京' == '正在上映的電影-上海'
E         - 正在上映的電影-上海
E         ?         ^^
E         + 正在上映的電影-北京
E         ?         ^^
?
tests/test_in_theaters.py:20: AssertionError
==================================================== short test summary info =====================================================
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[\u9a8c\u8bc1\u54cd\u5e94\u4e2dstart\u548ccount\u4e0e\u8bf7\u6c42\u4e2d\u7684\u53c2\u6570\u4e00\u81f4]
================================================== 1 failed, 1 passed in 0.69s ===================================================

從結果看,Pytest收集到了2個items,測試腳本執(zhí)行了兩遍,第一遍執(zhí)行用第一組測試數(shù)據(jù),結果是失敗(F),第二遍執(zhí)行用第二組測試數(shù)據(jù),結果是通過(.)。執(zhí)行完成后的summary info部分,看到了一些Unicode編碼,這里其實是ids的內(nèi)容,因為是中文,所以默認這里顯示Unicode編碼。為了顯示中文,需要在測試項目的根目錄下創(chuàng)建一個Pytest的配置文件pytest.ini,在其中添加如下代碼:

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True

再次執(zhí)行測試腳本,在測試結果的summary_info部分,則會顯示正確中文內(nèi)容了。

FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[驗證響應中start和count與請求中的參數(shù)一致] - AssertionError: ...

按照這種參數(shù)化的方法,如果想修改或者添加測試數(shù)據(jù),只需要修改測試數(shù)據(jù)文件即可。

現(xiàn)在,自動化測試項目的目錄結構應該是如下這樣:

$ tree
.
├── Pipfile
├── Pipfile.lock
├── config
├── data
│   └── test_in_theaters.yaml
├── pytest.ini
├── tests
│   ├── test_in_theaters.py
│   └── test_time.py
└── utils
    └── commlib.py
?
4 directories, 7 files

07 — 測試配置管理

06小節(jié)的自動化測試代碼中,host是寫在測試腳本中的,這種硬編碼方式顯然是不合適的。這個host在不同的測試腳本都會用到,應該放到一個公共的地方來維護。如果需要對其進行修改,那么只需要修改一個地方就可以了。根據(jù)我的實踐經(jīng)驗,將其放到config文件夾中,是比較好的。

除了host外,其他與測試環(huán)境相關的配置信息也可以放到config文件夾中,比如數(shù)據(jù)庫信息、kafka連接信息等,以及與測試環(huán)境相關的基礎測試數(shù)據(jù),比如測試賬號。很多時候,我們會有不同的測試環(huán)境,比如dev環(huán)境、test環(huán)境、stg環(huán)境、prod環(huán)境等。我們可以在config文件夾下面創(chuàng)建子目錄來區(qū)分不同的測試環(huán)境。因此config文件夾,應該是類似這樣的結構:

├── config
│   ├── prod
│   │   └── config.yaml
│   └── test
│       └── config.yaml

在config.yaml中存放不同環(huán)境的配置信息,以前面的例子為例,應該是這樣:

host:
  douban: http://api.douban.com

將測試配置信息從腳本中拆分出來,就需要有一種機制將其讀取到,才能在測試腳本中使用。Pytest提供了fixture機制,通過它可以在測試執(zhí)行前執(zhí)行一些操作,在這里我們利用fixture提前讀取到配置信息。我們先對官方文檔上的例子稍加修改,來介紹fixture的使用。請看下面的代碼:

import pytest
?
?
@pytest.fixture
def smtp_connection():
    import smtplib
    connection = smtplib.SMTP_SSL("smtp.163.com", 465, timeout=5)
    yield connection
    print("teardown smtp")
    connection.close()
?
?
def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert 0

這段代碼中,smtp_connection被裝飾器@pytest.fixture裝飾,表明它是一個fixture函數(shù)。這個函數(shù)的功能是連接163郵箱服務器,返回一個連接對象。當test_ehlo的最后一次測試執(zhí)行完成后,執(zhí)行print(“teardown smtp”)和connection.close()斷開smtp連接。

fixture函數(shù)名可以作為測試方法test_ehlo的參數(shù),在測試方法內(nèi)部,使用fixture函數(shù)名這個變量,就相當于是在使用fixture函數(shù)的返回值。

回到我們讀取測試配置信息的需求上,在自動化測試項目tests/目錄中創(chuàng)建一個文件conftest.py,定義一個fixture函數(shù)env:

@pytest.fixture(scope="session")
def env(request):
    config_path = os.path.join(request.config.rootdir, 
                               "config", 
                               "test", 
                               "config.yaml")
    with open(config_path) as f:
        env_config = yaml.load(f.read(), Loader=yaml.SafeLoader)
    return env_config

conftest.py文件是一個plugin文件,里面可以實現(xiàn)Pytest提供的Hook函數(shù)或者自定義的fixture函數(shù),這些函數(shù)只在conftest.py所在目錄及其子目錄中生效。scope="session"表示這個fixture函數(shù)的作用域是session級別的,在整個測試活動中開始前執(zhí)行,并且只會被執(zhí)行一次。除了session級別的fixture函數(shù),還有function級別、class級別等。

env函數(shù)中有一個參數(shù)request,其實request也是一個fixture函數(shù)。在這里用到了它的request.config.rootdir屬性,這個屬性表示的是pytest.ini這個配置文件所在的目錄,因為我們的測試項目中pytest.ini處于項目的根目錄,所以config_path的完整路徑就是:

/Users/chunming.liu/learn/api_pytest/config/test/config.yaml

將env作為參數(shù)傳入測試方法test_in_theaters,將測試方法內(nèi)的host改為env[“host”][“douban”]:

class TestInTheaters(object):
    @pytest.mark.parametrize("case,http,expected", list(list_params), ids=cases)
    def test_in_theaters(self, env, case, http, expected):
        r = requests.request(http["method"],
                             url=env["host"]["douban"] + http["path"],
                             headers=http["headers"],
                             params=http["params"])
        response = r.json()

這樣就達到了測試配置文件與測試腳本相互分離的效果,如果需要修改host,只需要修改配置文件即可,測試腳本文件就不用修改了。修改完成后執(zhí)行測試的方法不變。

上面的env函數(shù)實現(xiàn)中,有點點小缺憾,就是讀取的配置文件是固定的,讀取的都是test環(huán)境的配置信息,我們希望在執(zhí)行測試時,通過命令行選項,可指定讀取哪個環(huán)境的配置,以便在不同的測試環(huán)境下開展測試。Pytest提供了一個叫作pytest_addoption的Hook函數(shù),可以接受命令行選項的參數(shù),寫法如下:

def pytest_addoption(parser):
    parser.addoption("--env",
                     action="store",
                     dest="environment",
                     default="test",
                     help="environment: test or prod")

pytest_addoption的含義是,接收命令行選項–env選項的值,存到environment變量中,如果不指定命令行選項,environment變量默認值是test。將上面代碼也放入conftest.py中,并修改env函數(shù),將os.path.join中的"test"替換為request.config.getoption(“environment”),這樣就可以通過命令行選項來控制讀取的配置文件了。比如執(zhí)行test環(huán)境的測試,可以指定–env test:

$ py.test --env test tests/test_in_theaters.py

如果不想每次都在命令行上指定–env,還可以將其放入pyest.ini中:

[pytest]
addopts = --env prod

命令行上的參數(shù)會覆蓋pyest.ini里面的參數(shù)。

08 — 測試的準備與收尾

很多時候,我們需要在測試用例執(zhí)行前做數(shù)據(jù)庫連接的準備,做測試數(shù)據(jù)的準備,測試執(zhí)行后斷開數(shù)據(jù)庫連接,清理測試臟數(shù)據(jù)這些工作。通過07小節(jié)大家對于通過env這個fixture函數(shù),如何在測試開始前的開展準備工作有所了解,本小節(jié)將介紹更多內(nèi)容。

@pytest.fixture函數(shù)的scope可能的取值有function,class,module,package 或 session。他們的具體含義如下:

  • function,表示fixture函數(shù)在測試方法執(zhí)行前和執(zhí)行后執(zhí)行一次。
  • class,表示fixture函數(shù)在測試類執(zhí)行前和執(zhí)行后執(zhí)行一次。
  • module,表示fixture函數(shù)在測試腳本執(zhí)行前和執(zhí)行后執(zhí)行一次。
  • package,表示fixture函數(shù)在測試包(文件夾)中第一個測試用例執(zhí)行前和最后一個測試用例執(zhí)行后執(zhí)行一次。
  • session,表示所有測試的最開始和測試結束后執(zhí)行一次。

通常,數(shù)據(jù)庫連接和斷開、測試配置文件的讀取等工作,是需要放到session級別的fixture函數(shù)中,因為這些操作針對整個測試活動只需要做一次。而針對測試數(shù)據(jù)的準備,通常是function級別或者class級別的,因為測試數(shù)據(jù)針對不同的測試方法或者測試類往往都不相同。

在TestInTheaters測試類中,模擬一個準備和清理測試數(shù)據(jù)的fixture函數(shù)preparation,scope設置為function:

@pytest.fixture(scope="function")
    def preparation(self):
        print("在數(shù)據(jù)庫中準備測試數(shù)據(jù)")
        test_data = "在數(shù)據(jù)庫中準備測試數(shù)據(jù)"
        yield test_data
        print("清理測試數(shù)據(jù)")

在測試方法中,將preparation作為參數(shù),通過下面的命令執(zhí)行測試:

$ pipenv py.test -s -q --tb=no tests/test_in_theaters.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
collected 2 items                                                
tests/test_in_theaters.py 在數(shù)據(jù)庫中準備測試數(shù)據(jù)
F清理測試數(shù)據(jù)
在數(shù)據(jù)庫中準備測試數(shù)據(jù)
.清理測試數(shù)據(jù)
?
==================================================== short test summary info =====================================================
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[驗證響應中start和count與請求中的參數(shù)一致] - AssertionError: ...
================================================== 1 failed, 1 passed in 0.81s ===================================================

通過輸出可以看到在每一條測試用例執(zhí)行前后,各執(zhí)行了一次“在數(shù)據(jù)庫中準備測試數(shù)據(jù)”和“清理測試數(shù)據(jù)”。如果scope的值改為class,執(zhí)行測試用例的輸出信息將是下面這樣:

tests/test_in_theaters.py 在數(shù)據(jù)庫中準備測試數(shù)據(jù)
F.清理測試數(shù)據(jù)
在測試類執(zhí)行前后各執(zhí)行一次“在數(shù)據(jù)庫中準備測試數(shù)據(jù)”和“清理測試數(shù)據(jù)”。

09 — 標記與分組

通過pytest.mark可以給測試用例打上標記,常見的應用場景是:針對某些還未實現(xiàn)的功能,將測試用例主動跳過不執(zhí)行?;蛘咴谀承l件下,測試用例跳過不執(zhí)行。還有可以主動將測試用例標記為失敗等等。針對三個場景,pytest提供了內(nèi)置的標簽,我們通過具體代碼來看一下:

import sys
?
import pytest
?
?
class TestMarks(object):
    @pytest.mark.skip(reason="not implementation")
    def test_the_unknown(self):
        """
        跳過不執(zhí)行,因為被測邏輯還沒有被實現(xiàn)
        """
        assert 0
?
    @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
    def test_skipif(self):
        """
        低于python3.7版本不執(zhí)行這條測試用例
        :return:
        """
        assert 1
?
    @pytest.mark.xfail
    def test_xfail(self):
        """
        Indicate that you expect it to fail
        這條用例失敗時,測試結果被標記為xfail(expected to fail),并且不打印錯誤信息。
        這條用例執(zhí)行成功時,測試結果被標記為xpassed(unexpectedly passing)
        """
        assert 0
?
    @pytest.mark.xfail(run=False)
    def test_xfail_not_run(self):
        """
        run=False表示這條用例不用執(zhí)行
        """
        assert 0

下面來運行這個測試:

$ py.test -s -q --tb=no tests/test_marks.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
collected 4 items                                                                                                                
?
tests/test_marks.py s.xx
============================================ 1 passed, 1 skipped, 2 xfailed in 0.06s =============================================

從結果中可以看到,第一條測試用例skipped了,第二條測試用例passed了,第三條和第四條測試用例xfailed了。

除了內(nèi)置的標簽,還可以自定義標簽并加到測試方法上:

@pytest.mark.slow
    def test_slow(self):
        """
        自定義標簽
        """
        assert 0

這樣就可以通過-m過濾或者反過濾,比如只執(zhí)行被標記為slow的測試用例:

$ py.test -s -q --tb=no -m "slow" tests/test_marks.py
$ py.test -s -q --tb=no -m "not slow" tests/test_marks.py

對于自定義標簽,為了避免出現(xiàn)PytestUnknownMarkWarning,最好在pytest.ini中注冊一下:

[pytest]
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')

10 — 并發(fā)執(zhí)行

如果自動化測試用例數(shù)量成千上萬,那么并發(fā)執(zhí)行它們是個很好的主意,可以加快整體測試用例的執(zhí)行時間。

pyest有一個插件pytest-xdist可以做到并發(fā)執(zhí)行,安裝之后,執(zhí)行測試用例通過執(zhí)行-n參數(shù)可以指定并發(fā)度,通過auto參數(shù)自動匹配CPU數(shù)量作為并發(fā)度。并發(fā)執(zhí)行本文的所有測試用例:

$ py.test -s -q --tb=no -n auto tests/
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
plugins: xdist-1.31.0, forked-1.1.3
gw0 [10] / gw1 [10] / gw2 [10] / gw3 [10] / gw4 [10] / gw5 [10] / gw6 [10] / gw7 [10]
s.FxxF..F.
==================================================== short test summary info =====================================================
FAILED tests/test_marks.py::TestMarks::test_slow - assert 0
FAILED tests/test_smtpsimple.py::test_ehlo - assert 0
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[驗證響應中start和count與請求中的參數(shù)一致] - AssertionError: ...
======================================= 3 failed, 4 passed, 1 skipped, 2 xfailed in 1.91s ========================================

可以非常直觀的感受到,并發(fā)執(zhí)行比順序執(zhí)行快得多。但是并發(fā)執(zhí)行需要注意的是,不同的測試用例之間不要有測試數(shù)據(jù)的相互干擾,最好不同的測試用例使用不同的測試數(shù)據(jù)。

這里提一下,pytest生態(tài)中,有很多第三方插件很好用,更多的插件可以在這里https://pypi.org/search/?q=pytest-查看和搜索,當然我們也可以開發(fā)自己的插件。

11 — 測試報告

Pytest可以方便的生成測試報告,通過指定–junitxml參數(shù)可以生成XML格式的測試報告,junitxml是一種非常通用的標準的測試報告格式,可以用來與持續(xù)集成工具等很多工具集成:

$ py.test -s -q --junitxml=./report.xml tests/

現(xiàn)在應用更加廣泛的測試報告是Allure,可以方便的與Pytest集成,大家可以參考我的另外一篇公眾號文章《用Pytest+Allure生成漂亮的HTML圖形化測試報告》。

12 — 總結

本文章以實際項目出發(fā),介紹了如何編寫測試用例、如何參數(shù)化、如何進行測試配置管理、如何進行測試的準備和清理,如何進行并發(fā)測試并生成報告。根據(jù)本文的介紹,你能夠逐步搭建起一套完整的測試項目。

本文并沒有對Pytest的細節(jié)和比較高階的內(nèi)容做充分介紹,以后再進行專題介紹,這篇文章主要目的是讓大家能夠將Pytest用起來。更高階的內(nèi)容,公眾號后續(xù)文章還將繼續(xù)對其進行介紹。至此,我們的自動化測試項目完整目錄結構如下:

$ tree
.
├── Pipfile
├── Pipfile.lock
├── config
│   ├── prod
│   │   └── config.yaml
│   └── test
│       └── config.yaml
├── data
│   └── test_in_theaters.yaml
├── pytest.ini
├── tests
│   ├── conftest.py
│   ├── test_in_theaters.py
│   ├── test_marks.py
│   ├── test_smtpsimple.py
│   └── test_time.py
└── utils
    └── commlib.py
?
6 directories, 12 files

參考資料

[1] https://docs.pytest.org/en/latest/

[2] https://www.guru99.com/pytest-tutorial.html?

到此這篇關于淺談基于Pytest框架的自動化測試開發(fā)實踐的文章就介紹到這了,更多相關Pytest自動化測試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • python 在服務器上調(diào)用數(shù)據(jù)庫特別慢的解決過程

    python 在服務器上調(diào)用數(shù)據(jù)庫特別慢的解決過程

    這篇文章主要介紹了python 在服務器上調(diào)用數(shù)據(jù)庫特別慢的解決過程,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Python中getservbyport和getservbyname函數(shù)的用法大全

    Python中getservbyport和getservbyname函數(shù)的用法大全

    在Python的網(wǎng)絡編程中,getservbyport()函數(shù)和getservbyname()函數(shù)是socket模塊中的兩個函數(shù),因此在使用這兩個函數(shù)時,需要導入socket模塊,這篇文章主要介紹了Python中getservbyport和getservbyname函數(shù)的用法,需要的朋友可以參考下
    2023-01-01
  • Python 實現(xiàn)淘寶秒殺的示例代碼

    Python 實現(xiàn)淘寶秒殺的示例代碼

    本篇文章主要介紹了Python 實現(xiàn)淘寶秒殺的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • 更改Python的pip install 默認安裝依賴路徑方法詳解

    更改Python的pip install 默認安裝依賴路徑方法詳解

    今天小編就為大家分享一篇更改Python的pip install 默認安裝依賴路徑方法詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-10-10
  • NetWorkX使用方法及nx.draw()相關參數(shù)解讀

    NetWorkX使用方法及nx.draw()相關參數(shù)解讀

    這篇文章主要介紹了NetWorkX使用方法及nx.draw()相關參數(shù)解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • python使用自定義釘釘機器人的示例代碼

    python使用自定義釘釘機器人的示例代碼

    這篇文章主要介紹了python使用自定義釘釘機器人,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-06-06
  • python之super的使用小結

    python之super的使用小結

    這篇文章主要介紹了python之super的使用小結,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-08-08
  • Flask之pipenv虛擬環(huán)境的實現(xiàn)

    Flask之pipenv虛擬環(huán)境的實現(xiàn)

    這篇文章主要介紹了Flask之pipenv虛擬環(huán)境的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-11-11
  • python線程、進程和協(xié)程詳解

    python線程、進程和協(xié)程詳解

    Python被人詬病最多的大概就是性能差,在這里講一下 Python 的多進程,多線程與協(xié)程。首先聲明這不是教程,看完這篇文章,大概能夠對 Python 的多進程與多線程有一定的了解。
    2016-07-07
  • 基于Pytorch實現(xiàn)邏輯回歸

    基于Pytorch實現(xiàn)邏輯回歸

    這篇文章主要為大家詳細介紹了基于Pytorch實現(xiàn)邏輯回歸,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-07-07

最新評論