1. 程式人生 > >pytest單元測試框架外掛開發實踐

pytest單元測試框架外掛開發實踐

我學習了一下pytest。

按照慣例,檢驗我學習成果的方法就是造個輪子,於是我根據寫單元測試時發現的痛點,寫了一個外掛

https://github.com/ShichaoMa/pytest-apistellar

現在分享出來,供大家學習和參考。

既然說到痛點,想必寫過單元測試的人都知道,由於我們有時無法直接訪問資料庫等服務,我們需要mock掉一些方法和屬性,但mock是一個很痛苦的事情,而且寫起來也相當於不優雅,而我這個外掛解決的主要問題就是減輕mock帶來的痛苦。通過一個裝飾器,mock資料簡直不費吹灰之力。

看名字你肯定可以猜的出來,這個外掛其實是為我的非同步web框架開發的:

https://github.com/ShichaoMa/apistellar

沒錯,apistellar前身就是star-builder。我之前寫過幾篇文章介紹過他。因為同事告訴我star-builder簡稱SB太過於美妙,於是一氣之下我就把這個包重新命名了。現在apistellar在我們公司內部已經得到了廣泛使用,作為少有的ASGI框架,有興趣的同學可以學習一下。

言歸正傳,作為apistellar的測試外掛,最初肯定是為apistellar服務的,但後來感覺mock是所有人的痛點,由於我們公司好多服務還是基於python2的,於是我針對mock這個功能做了一下相容。當然mock只是其功能之一,同時他還支援針對apistellar web服務的介面測試,簡而言之,他可以啟動一個apistellar的服務來測試某個模組提供的RESTful介面。

當然這節主講更通用的單元測試mock。

現在我們有一個模組,他的包地址為file.file::File

class File(object):

    def __init__(self, filename):
        self.filename = filename

    @classmethod
    def load(cls):
        return cls(filename="這個檔案是從資料庫獲取的")

    @classmethod
    async def load_async(cls):
        return cls(filename="這個檔案是從資料庫獲取的")

其中的兩個load方法都資料庫中獲取資料返回一個File物件,現在我們需要mock掉load操作,讓其返回我們渴望的資料。

怎麼辦?

pytest傳統的做法應該是這樣:

from  file.file import File
def test_load(monkeypatch):
    def load():
        return File("同步返回的檔案")
    with monkeypatch.context() as m:
        m.setattr(File, "load", load)
        assert File.load().filename == "同步返回的檔案"

    async def load_async():
        return File("非同步返回的檔案")
    with monkeypatch.context() as m:
        m.setattr(File, "load_async", load_async)
        assert (await File.load_async()).filename == "非同步返回的檔案"

我們需要使用monkeypatch改掉我們想mock的那個方法和屬性,這樣的實現很不優雅,存在很多多餘的函式,讓程式碼顯示相當冗餘。

現在不同了,我們有了pytest-apistellar,讓我們看看他是怎麼做的:

######為了發現在當前目錄下建立的file包手動將當前目錄加入環境變數######
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
#####正常測試我們的專案應該已存在於pythonpath中所以不需要這麼寫######
import pytest

from file.file import File


# 如果mock掉的方法不需要邏輯判斷,直接使用ret_val指定返回值即可
@pytest.mark.usefixtures("mock")
@pytest.mark.prop("file.file.File.load", ret_val=File("直接mock返回值"))
def test_load1():
    assert File.load().filename == "直接mock返回值"


# 無論被mock的方法是非同步還是同步,無腦指定返回值
# 不過非同步的方法測試需要pytest-asyncio這個包支援,指定一個asyncio的mark才可以
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock")
@pytest.mark.prop("file.file.File.load_async", ret_val=File("非同步方法也能mock"))
async def test_load2():
    assert (await File.load_async()).filename == "非同步方法也能mock"


# 如果我們用來替代的mock方法有一定的邏輯,我們可以指定一個ret_factory,指向替代方法的包地址
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock")
@pytest.mark.prop("file.file.File.load_async", ret_factory="factories.mock_load")
async def test_load3():
    assert (await File.load_async()).filename == "通過工廠mock"


# 這種就是有業務邏輯的替代方法,他可以通過filename不同返回不同的File物件
@pytest.mark.usefixtures("mock")
@pytest.mark.prop("file.file.File.load", ret_factory="factories.mock_load", filename="還能給工廠傳參")
def test_load4():
    assert File.load().filename == "還能給工廠傳參"


# 裝飾器太長怎麼辦,取個別名來縮短導包引數
from pytest_apistellar import prop_alias

file = prop_alias("file.file.File", "factories")


@pytest.mark.usefixtures("mock")
@file("load", ret_factory="mock_load", filename="使用別名裝飾器,把字首連起來")
def test_load4():
    assert File.load().filename == "使用別名裝飾器,把字首連起來"


# module作用域的mock
pytestmark = [
        file("load", ret_factory="mock_load", filename="這是一個module作用域的mock")
    ]


@pytest.mark.usefixtures("mock")
def test_load5():
    assert File.load().filename == "這是一個module作用域的mock"


@pytest.mark.usefixtures("mock")
class TestLoad(object):
    # class作用域的mock
    pytestmark = [
        file("load", ret_factory="mock_load", filename="這是一個class作用域的mock")
    ]

    def test_load6(self):
        assert File.load().filename == "這是一個class作用域的mock"

    # funtion作用域的mock
    @file("load", ret_factory="mock_load", filename="這是一個function作用域的mock")
    def test_load7(self):
        assert File.load().filename == "這是一個function作用域的mock"

factories模組的程式碼在這:

from file.file import File


def mock_load(filename="通過工廠mock"):
    return File(filename)

正如註釋所寫,pytest-apistellar支援五個pytest作用域的mock,對於簡單mock,直接在裝飾器加入返回值資料就可以。同時pytest-apistellar還支援mock環境變數。具體的可以自行檢視README。

https://github.com/ShichaoMa/pytest-apistellar

感謝閱讀!