Bootstrap

通过pytest-xdist插件并发执行用例时, scope=session的fixture会运行多次问题的解决方案

场景

在UI自动化项目中,使用的是pytest + playwright, 需要实现同一个用户只登录一次的,所以在conftest.py中定义了一个scope=session的fixture,然后在此fixture中实现了系统登录,在非并发模式下执行的时候,能保证同一个用户只登录一次,但是并发执行的时候每个case都会执行登录

问题

需要解决并发执行的时候同一个用户只登录一次,如果是不同用户的时候还是需要进行登录

解决方法

  1. pytest-xdist官方给出的解决方案:https://pytest-xdist.readthedocs.io/en/stable/how-to.html#making-session-scoped-fixtures-execute-only-once
  2. 参考pytest-xdist给出的解决方案实现如下:
import pytest
from filelock import FileLock
from os import path
import os

@pytest.fixture(scope="session", autouser=True)
def login_session(browser, browser_context_args, session_file, worker_id):
    ''' 
        1. 并发执行情况下,同一个user不需要多次登录
        2. 不同的用户还需要登录
        3. session_file是自定义的fixture,定义了用户登录信息存储的文件路径
    '''
    logging.debug("worker_id:%s" % worker_id)
    if path.exists(session_file) and worker_id == "master":
        logging.info("session file已存在,用户已登录")  # session文件已存在,不需要登录

    with FileLock(str(session_file) + ".lock"): #FileLock实现只登录一次
        if session_file.is_file():
            logging.info("session file已存在") # session 文件已存在,需要登录
        else:
            logging.info("用户进行登录操作")
            browsercontext = browser.new_context(**browser_context_args)
            login(browsercontext, request, session_file) # 函数定义了登录操作,并且登录成功后存储用户的session文件

# session文件路径的定义
@pytest.fixture(scope="session")
def set_session_file(user, tmp_path_factory, test_data):
    '''
    1.设置登录后的session保存位置, 通过tmp_path_factory设置临时目录
    '''
    logging.info(f"登录用户:{user}")
    # 这里取的临时目录的getbasetemp().parent,因为并发时getbasetemp获取每个case路径是类似temp_dir/gw0, temp_dir/gw1,所以为了保证存放session的路径是同一个路径; 
    #当然这里也可以创建一个目录来存放session,不过要在login_session的fixture中添加yield并且在yield后添加删除session file,避免session失效的情况, 使用临时目录会自动删除
    if "gw" in path.split(tmp_path_factory.getbasetemp())[-1]:  #并发执行的路径取值
        dir= tmp_path_factory.getbasetemp().parent
    else: 非并发执行的时候路径取值
        dir = tmp_path_factory.getbasetemp()
    session_file = path.join(dir, "session_files", "%s.json" % user) 
    logging.info('session file is:%s'%session_file)
    return Path(session_file)
;