Skip to content

好用的 pytest 之 fixture (2)

📅 6/22/2024

本篇接著上一篇繼續介紹 fixture

scope 的好處

之前我們都是透過 redis 的讀寫當成範例,而每一項單元測試中都會用到 redis_conn 來獲取連線; 然而我們並沒有必要一直重複和 redis 建立連線,甚至同頭用到尾都沒有關係,因此可以透過設定作用域 scope 來減少 fixture 建立和銷毀的次數

python
@pytest.fixture(scope='session')
def redis_conn():
    print('create')  # print 看看有無設置 scope 的差別
    return create_redis()

@pytest.mark.parametrize(
    ('username', 'value'),
    [('Nick', 1), ('foo', 2), ('bar', 3), ('baz', 4)]
)
def test_redis_get(redis_conn, username, value):
    assert int(redis_conn.get(name=f'user:{username}')) == value
  • scope='session'

  • scope='function'

以下整理作用域和他的影響範圍

作用域sessionpackagemoduleclassfunction
影響範圍整個測試一個資料夾一個py檔案一個類一個函式

conftest.py

在進入程式碼範例之前,我們先把一些常用的夾具放到 conftest.py,像是我把上面範例用到的 redis_conn 放到 conftest.py ,放到這裡面的夾具會在執行pytest進入測試時自動載入, 後續測試時就不需要在特別 import 也可以使用。

至於 conftest.py 要放在哪裏就要看你希望它影響那些範圍的測試了。

package

作用域設定為 package 會在測試中的每個資料夾的第一個測試建立並在最後一個測試銷毀,以下為檔案結構和範例

  • 結構:

    test
    │  conftest.py
    │  pytest.ini
    │  __init__.py  
    ├─db
    │  │  test_redis.py
    │  └─ __init__.py
    
    └─db2
        │  test_redis.py
        └─ __init__.py
  • conftest.py

    python
    @pytest.fixture(scope='package')
      def redis_conn(request):
      package_name = os.path.basename(os.path.dirname(request.fspath))
      print(f'Start In {package_name=}')
      yield create_redis()
      print(f'End In {package_name=}')

接著執行測試指令 pytest test/db test/db2 -sv

到這裡出現了我意料之外的事情,本來我預期會在 dbdb2 這兩個 package 會各建立一次連線;但實際上它卻只建立一次,這不就和設定作用域為 session 一樣了嗎?

我猜想可能是那個 conftest.py 的位置是在最開始的那層,所以 pytest 把 dbdb2 都當成是 test package 的一部分了;既然這樣我們就把 conftest 複製到 dbdb2 內吧。

  • 結構:
    test
    │  conftest.py
    │  pytest.ini
    │  __init__.py  
    ├─db
    │  │  conftest.py
    │  │  test_redis.py
    │  └─ __init__.py
    
    └─db2
        │  conftest.py
        │  test_redis.py
        └─ __init__.py

module

作用域設定為 module 會在測試中的每個py檔案的第一個測試建立並在最後一個測試銷毀,以下為檔案結構和範例

  • 結構:

    test
    │  conftest.py
    │  pytest.ini
    │  __init__.py
    └─db
      │  test_redis.py
      │  test_redis_2.py
      └─__init__.py
  • conftest.py

    python
    @pytest.fixture(scope='package')
    def redis_conn(request):
      module_name = os.path.basename(request.fspath)
      print(f'Start In {module_name=}')
      yield create_redis()
      print(f'End In {module_name=}')

class

作用域設定為 module 會在測試類的第一個測試方法建立並在最後一個測試方法銷毀,以下為範例

  • conftest.py

    python
    @pytest.fixture(scope='class')
    def redis_conn(request):
        class_name = request.cls.__name__
        print(f'Start In {class_name=}')
        yield create_redis()
        print(f'End In {class_name=}')
  • test_class.py

    python
    class TestRedisSet:
    
        @pytest.mark.parametrize(
            ('username', 'value'),
            [('Nick', 1), ('foo', 2), ('bar', 3), ('baz', 4)]
        )
        def test_redis_set(self, redis_conn, username, value):
            redis_conn.set(name=f'user:{username}', value=value)
    
    
    class TestRedisGet:
    
        @pytest.mark.parametrize(
            ('username', 'value'),
            [('Nick', 1), ('foo', 2), ('bar', 3), ('baz', 4)]
        )
        def test_redis_get(self, redis_conn, username, value):
            assert int(redis_conn.get(name=f'user:{username}')) == value

function

作用域設定為 function 會在第一個測試項建立並在最後一個測試項銷毀,只要不特別設定 scope 就是用 function 作為預設值,以下為範例

  • conftest.py

    python
    @pytest.fixture
    def redis_conn(request):
        _id = request.node.callspec.id
        print(f'Start In {_id=}')
        yield create_redis()
        print(f'End In {_id=}')
  • test_redis.py

    python
    @pytest.mark.parametrize(
        ('username', 'value'),
        [('Nick', 1), ('foo', 2), ('bar', 3), ('baz', 4)],
        ids=['first test', 'second test', 'third test', 'fourth test']
    )
    def test_redis_get(redis_conn, username, value):
        assert int(redis_conn.get(name=f'user:{username}')) == value