【pytest官方文件】解讀fixtures - 11. fixture的執行順序,3要素詳解(長文預警)

把蘋果v咬哭發表於2021-04-26

當pytest要執行一個測試函式,這個測試函式還請求了fixture函式,那麼這時候pytest就要先確定fixture的執行順序了。

影響因素有三:

  • scope,就是fixture函式的作用範圍,比如scope='class'
  • dependencies,可能會存在fixture請求了別的fixture,所以產生了依賴關係,也要考慮進去。
  • autouse,如果autouse=True,那麼在作用範圍內,這個fixture是最先呼叫的。

所以,像fixture函式或測試函式的名稱、定義的位置、定義的順序以及請求fixture的順序,除了巧合之外,對執行順序沒有任何影響。

對於這些巧合情況,雖然pytest會盡力保持每次執行的順序都一樣,但是也難免會有意外。所以,如果我們想控制好順序,最安全的方法還是
依賴上述三點,並且要弄清依賴關係。

一、使用範圍更大的fixture函式優先執行

更大範圍(比如session)的fixture會在小範圍(比如函式或類)之前執行。

程式碼示例:

import pytest


@pytest.fixture(scope="session")
def order():
    return []


@pytest.fixture
def func(order):
    order.append("function")


@pytest.fixture(scope="class")
def cls(order):
    order.append("class")


@pytest.fixture(scope="module")
def mod(order):
    order.append("module")


@pytest.fixture(scope="package")
def pack(order):
    order.append("package")


@pytest.fixture(scope="session")
def sess(order):
    order.append("session")


class TestClass:
    def test_order(self, func, cls, mod, pack, sess, order):
        assert order == ["session", "package", "module", "class", "function"]

執行結果:

test_module1.py .                                                        [100%]

============================== 1 passed in 0.01s ==============================
Process finished with exit code 0

既然執行通過,那麼這些fixture函式的執行順序就是列表裡的順序["session", "package", "module", "class", "function"]

二、相同順序的fixture基於依賴項執行

當一個fixture函式請另一個fixture函式,另一個會先執行。

比如,fixturea請求fixtureb,需要用b返回的結果。那麼b先執行,因為a依賴於b,必須得讓b先執行,否則a就沒法幹活。

另外,即使a不需要用b返回的結果,只要a需要確保在b之後執行,a仍然可以通過請求b來控制順序。

1.請求依賴呈線性情況下

程式碼示例:

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def a(order):
    order.append("a")


@pytest.fixture
def b(a, order):
    order.append("b")


@pytest.fixture
def c(a, b, order):
    order.append("c")


@pytest.fixture
def d(c, b, order):
    order.append("d")


@pytest.fixture
def e(d, b, order):
    order.append("e")


@pytest.fixture
def f(e, order):
    order.append("f")


@pytest.fixture
def g(f, c, order):
    order.append("g")


def test_order(g, order):
    assert order == ["a", "b", "c", "d", "e", "f", "g"]

官方給出了上述程式碼的依賴關係圖(左)和執行順序圖(右)。

不要方,只要從測試函式test_order開始,一層一層跟著fixture的依賴一層一層梳理下去就對上了。

到這裡,其實也就能更進一步理解了,如果想控制好執行順序,就要給這些請求依賴提供足夠的資訊。
這樣pytest能夠找出一個清晰的線性依賴鏈,最終給呼叫它們的測試函式一個確定的操作順序。

2.請求依賴不呈線性的情況,會影響操作執行

此外,如果存在歧義,出現多種執行順序,那pytest可以在多種順序裡任選。

基於上述的請求依賴關係圖(左),假設d沒有請求c,那麼此時的依賴關係就變成了右圖所示:

可以看出:

  • c此時只被一個g請求。
  • g既請求了c,還請求了f。

因為c現在除了一個g,其他沒有別的依賴關係,所以現在pytest不知道c應該是在f,e之前執行,還是應該在d之後執行。
這時候,pytest就會認定,c可以在g和b之間的任何位置點執行。也就是說,c必須在b之後和g之前執行。

如果這種情況出現,那麼你預期的測試行為或者測試結果可能會受到影響。可以修改下程式碼,讓d中沒有請求c,並且我加了print,方便
看fixture的執行順序。
執行下程式碼:

test_module1.py 
執行order
執行a
執行b
執行d
執行e
執行f
執行c
執行g
F
demo\test_module1.py:51 (test_order)
['a', 'b', 'd...'f', 'c', ...] != ['a', 'b', 'c...'e', 'f', ...]

Expected :['a', 'b', 'c...'e', 'f', ...]
Actual   :['a', 'b', 'd...'f', 'c', ...]

會看到測試失敗了,因為fixture的執行順序變了,導致新增到order列表的元素順序也變了,實際與預期結果不相等,測試失敗。
不過可以從列印出的fixture執行順序看出,c確實在b之後和g之前執行了。

官方描述這些想要表達的什麼呢?
我覺得應該是這個,如果你希望精確控制執行順序,避免順序不對而造成執行操作或測試結果有誤,那麼就要給足請求依賴,好讓pytest
線性制定執行順序。

三、Autouse的fixtures,會優先執行

1. autouse的妙用

如果請求了一個autouse=True的fixture函式,那麼這個autouse的fixture函式會比請求的其他fixture都要先執行。

另外,如果fixture a是autouse的,而fixture b不是。而fixture a又請求fixture b,那麼fixture b也將變成autouse的fixture ,但僅適用於請求了a的測試。
其實這點也很好理解,既然a是要先執行的,a又請求了b,說明a依賴於b,那麼b自然也是要先於a執行的。

在上一個例子中,由於d沒有去請求c,導致依賴關係模糊,最後影響了執行結果。
但是如果c是autouse,那麼b和a也就自動變成了autouse,因為c依賴於b和a。所以,這時候,c,b,a都會在其他非autouse的fixture函式之前執行。

修改下程式碼,在c上加上autouse:

import pytest


@pytest.fixture
def order():
    print("\n執行order")
    return []


@pytest.fixture
def a(order):
    print("執行a")
    order.append("a")


@pytest.fixture
def b(a, order):
    print("執行b")
    order.append("b")


@pytest.fixture(autouse=True)
def c(a, b, order):
    print("執行c")
    order.append("c")


@pytest.fixture
def d(b, order):
    print("執行d")
    order.append("d")


@pytest.fixture
def e(d, b, order):
    print("執行e")
    order.append("e")


@pytest.fixture
def f(e, order):
    print("執行f")
    order.append("f")


@pytest.fixture
def g(f, c, order):
    print("執行g")
    order.append("g")


def test_order(g, order):
    assert order == ["a", "b", "c", "d", "e", "f", "g"]

執行結果:

test_module1.py 
執行order
執行a
執行b
執行c
執行d
執行e
執行f
執行g
.                                                        [100%]

============================== 1 passed in 0.01s ==============================
Process finished with exit code 0

執行順序正常了,從a到g。
它們的依賴關係圖變成了這樣:

因為c變成了autouse,所以在圖裡處於d之上的位置,這時候pytest又可以將執行順序線性化了。而且,c也讓b和a都變成了autouse的fixture。

2. autouse的慎用

在使用autouse的時候也要小心。
因為一個測試函式即使沒有直接請求一個autouse fixture,但是隻要這個測試函式在這個autouse的作用範圍內,那麼這個autouse就會自動執行。
看程式碼示例:

import pytest


@pytest.fixture(scope="class")
def order():
    return []


@pytest.fixture(scope="class", autouse=True)
def c1(order):
    order.append("c1")


@pytest.fixture(scope="class")
def c2(order):
    order.append("c2")


@pytest.fixture(scope="class")
def c3(order, c1):
    order.append("c3")


class TestClassWithC1Request:
    def test_order(self, order, c1, c3):
        assert order == ["c1", "c3"]


class TestClassWithoutC1Request:
    def test_order(self, order, c2):
        assert order == ["c1", "c2"]

執行程式碼,執行case是通過的,說明order == ["c1", "c2"]
可以看到,雖然類TestClassWithoutC1Request(官方寫的是TestClassWithC1Request,應該是錯了)沒有請求c1,但是c1還是在這個類裡執行了。

但是,僅僅是一個autouse的fixture請求了一個非autouse的話,其實這並不能說這個非autouse的fixture也成為了一個可以應用到上下文的fixture函式。
僅僅是適用於請求它的那個autouse fixture的作用範圍。
例如,看下面程式碼:

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def c1(order):
    order.append("c1")


@pytest.fixture
def c2(order):
    order.append("c2")


class TestClassWithAutouse:
    @pytest.fixture(autouse=True)
    def c3(self, order, c2):
        order.append("c3")

    def test_req(self, order, c1):
        assert order == ["c2", "c3", "c1"]

    def test_no_req(self, order):
        assert order == ["c2", "c3"]


class TestClassWithoutAutouse:
    def test_req(self, order, c1):
        assert order == ["c1"]

    def test_no_req(self, order):
        assert order == []

if __name__ == '__main__':
    pytest.main(['-s', 'test_module2.py'])

執行都是可以通過的,這裡的依賴關係圖是這樣的。

在類TestClassWithAutouse中:

  • test_reqtest_no_req是2個測試方法。
  • c3是autouse,並且請求了c2,所以c2也成了autouse。雖然2個測試並沒去請求c2c3,但是都執行了,而且在c1之前執行。

在這裡,c3請求了還order,同樣地,在c3的作用域內,order也扮演了autouse的存在。但是在TestClassWithoutAutouse,order就不是
autouse了,所以類TestClassWithoutAutouse中的test_no_req可以執行成功,因為order=[]

相關文章