1. 背景

  • 使用 pytest-xdist 分布式插件可以加快运行,充分利用机器多核 CPU 的优势
  • 将常用功能放到 fixture,可以提高复用性和维护性
  • 做接口自动化测试的时候,通常我们会将登录接口放到 fixture 里面,并且 scope 会设置为 session,让他全局只运行一次
  • 但是当使用 pytest-xdist 的时候,scope=session 的 fixture 无法保证只运行一次,官方也通报了这一问题

2. 官方描述

  • pytest-xdist 的设计使每个工作进程将执行自己的测试集合并执行所有测试子集,这意味着在不同的测试过程中,要求高级范围的 fixture(如:session)将会被多次执行,这超出了预期,在某些情况下可能是不希望的
  • 尽管 pytest-xdist 没有内置支持来确保 scope=session 的fixture 仅执行一次,但是可以通过使用锁定文件进行进程间通信来实现

3. 前置知识

3.1 pytest-xdist 分布式插件使用详细教程
16. 分布式测试插件之pytest-xdist的详细使用

pytest-xdist 分布式插件原理
17. pytest-xdist 分布式测试的原理和流程

fixture 的使用详细教程
4. fixture 的详细使用

官方文档
https://pypi.org/project/pytest-xdist/

4. 官方解决办法(直接套用就行)

  1. import json
  2. import pytest
  3. from filelock import FileLock
  4. @pytest.fixture(scope="session")
  5. def session_data(tmp_path_factory, worker_id):
  6. if worker_id == "master":
  7. # not executing in with multiple workers, just produce the data and let
  8. # pytest's fixture caching do its job
  9. return produce_expensive_data()
  10. # get the temp directory shared by all workers
  11. root_tmp_dir = tmp_path_factory.getbasetemp().parent
  12. fn = root_tmp_dir / "data.json"
  13. with FileLock(str(fn) + ".lock"):
  14. if fn.is_file():
  15. data = json.loads(fn.read_text())
  16. else:
  17. data = produce_expensive_data()
  18. fn.write_text(json.dumps(data))
  19. return data
  • 若某个 scope = session 的 fixture 需要确保只运行一次的话,可以用上面的方法,直接套用,然后改需要改的部分即可(这个后面详细讲解)
  • 官方原话:这项技术可能并非在每种情况下都适用,但对于许多情况下,它应该是一个起点,在这种情况下,对于 scope = session 的fixture 只执行一次很重要

5. 后续栗子的代码

5.1 项目结构

  1. xdist+fixture(文件夹)
  2. tmp(存放 allure 数据文件夹)
  3. conftest.py
  4. test_1.py
  5. test_2.py
  6. test_3.py
  7. __init__.py

5.2 test_1.py 代码

  1. import os
  2. def test_1(test):
  3. print("os 环境变量",os.environ['token'])
  4. print("test1 测试用例", test)

5.3 test_2.py 代码

  1. import os
  2. def test_2(test):
  3. print("os 环境变量",os.environ['token'])
  4. print("test2 测试用例", test)

5.4 test_3.py 代码

  1. import os
  2. def test_3(test):
  3. print("os 环境变量",os.environ['token'])
  4. print("test3 测试用例", test)

6. 未解决情况下的栗子

6.1 conftest.py 代码

  1. import os
  2. import pytest
  3. from random import random
  4. @pytest.fixture(scope="session")
  5. def test():
  6. token = str(random())
  7. print("fixture:请求登录接口,获取token", token)
  8. os.environ['token'] = token
  9. return token

6.2 运行命令

  1. pytest -n 3 --alluredir=tmp

6.3 查看 allure 报告

image.png
image.png
image.png
scope=session 的 fixture 很明显执行了三次,三个进程下的三个测试用例得到的数据不一样,明显不会是我们想要的结果

7. 使用官方解决方法的栗子

7.1 优化代码

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. __title__ =
  5. __Time__ = 2021/4/27 11:28
  6. __Author__ = 小菠萝测试笔记
  7. __Blog__ = https://www.cnblogs.com/poloyy/
  8. """
  9. import json
  10. import os
  11. import pytest
  12. from random import random
  13. from filelock import FileLock
  14. @pytest.fixture(scope="session")
  15. def test(tmp_path_factory, worker_id):
  16. # 如果是单机运行 则运行这里的代码块【不可删除、修改】
  17. if worker_id == "master":
  18. """
  19. 【自定义代码块】
  20. 这里就写你要本身应该要做的操作,比如:登录请求、新增数据、清空数据库历史数据等等
  21. """
  22. token = str(random())
  23. print("fixture:请求登录接口,获取token", token)
  24. os.environ['token'] = token
  25. # 如果测试用例有需要,可以返回对应的数据,比如 token
  26. return token
  27. # 如果是分布式运行
  28. # 获取所有子节点共享的临时目录,无需修改【不可删除、修改】
  29. root_tmp_dir = tmp_path_factory.getbasetemp().parent
  30. # 【不可删除、修改】
  31. fn = root_tmp_dir / "data.json"
  32. # 【不可删除、修改】
  33. with FileLock(str(fn) + ".lock"):
  34. # 【不可删除、修改】
  35. if fn.is_file():
  36. # 缓存文件中读取数据,像登录操作的话就是 token 【不可删除、修改】
  37. token = json.loads(fn.read_text())
  38. print(f"读取缓存文件,token 是{token} ")
  39. else:
  40. """
  41. 【自定义代码块】
  42. 跟上面 if 的代码块一样就行
  43. """
  44. token = str(random())
  45. print("fixture:请求登录接口,获取token", token)
  46. # 【不可删除、修改】
  47. fn.write_text(json.dumps(token))
  48. print(f"首次执行,token 是{token} ")
  49. # 最好将后续需要保留的数据存在某个地方,比如这里是 os 的环境变量
  50. os.environ['token'] = token
  51. return token

7.2 运行命令

  1. pytest -n 3 --alluredir=tmp

7.3 查看 allure 报告

image.png
image.png
image.png
可以看到 fixture 只执行了一次,不同进程下的测试用例共享一个数据 token

7.4 重点

  • 读取缓存文件并不是每个测试用例都会读,它是按照进程来读取的
  • 比如 -n 3 指定三个进程运行,那么有一个进程会执行一次 fixture(随机),另外两个进程会各读一次缓存
  • 假设每个进程有很多个用例,那也只是读一次缓存文件,而不会读多次缓存文件
  • 所以最好要将从缓存文件读出来的数据保存在特定的地方,比如上面代码的 os.environ 可以将数据保存在环境变量中

8. 两个进程跑三个测试用例文件

还是上面栗子的代码

8.1 运行命令

  1. pytest -n 2 --alluredir=tmp

8.2 运行结果

image.png
image.png
image.png
可以看到 test_3 的测试用例就没有读缓存文件了,每个进程只会读一次缓存文件,记住哦!