如何在Python Hypothesis中实现测试示例的GUI可视化展示
Hypothesis测试示例可视化需求
- 针对每个测试用例(如
def test_a(a: int)、def test_b(b: str)),收集测试过程中生成的随机示例 - 单个测试的所有示例执行完成后,随机抽取5个示例生成GUI窗口展示,阻塞至窗口关闭后再执行下一个测试
- 若测试捕获到异常,立即基于已收集的示例弹出GUI
现有实现的问题
目前通过execute_example方法实现,但存在明显缺陷:
- 单个测试会多次触发GUI展示
- 最后一个测试无法触发GUI展示
现有实现代码:
# It adds the scenario returned by the test into a collection of scenarios # to be used for a GUI demo, and displays some of the related scenarios whenever # the test changes or an exception occurs. def execute_example(self, fn): def viewScens(viewDesc): scens = self.scenarios[viewDesc] if scens: numScens = min(NUM_VIZ, len(scens)) print(f"showing {numScens} '{viewDesc}' scenarios") testGUI = TestGUI(random.sample(scens, numScens)) testGUI.run() self.curDesc = None self.curScen = None try: ret = fn() if self.curDesc != self.lastDesc: if self.curDesc in self.seenDescs: # PROBLEM: this happens several times per test print(f"'{self.curDesc}' has been seen before") # PROBLEM: this won't ever be called for the last test! viewScens(self.lastDesc) return ret except Exception as e: # UnsatisfiedAssumption is used to break out of an example where the preconditions aren't met if not isinstance(e, UnsatisfiedAssumption): print(f"exception with '{self.curDesc}'") viewScens(self.curDesc) raise e finally: self.seenDescs.add(self.curDesc) if isinstance(self.curScen, TestScenario): self.scenarios[self.curDesc].append(self.curScen) self.lastDesc = self.curDesc
优化实现方案
1. 基于Hypothesis生命周期钩子实现
Hypothesis提供了测试生命周期相关的事件钩子,这是最可靠的实现方式:
- 注册
start_test事件:新测试开始时,先展示上一个测试的示例(如果有),再初始化当前测试的示例集合 - 注册
failed_test事件:测试抛出非UnsatisfiedAssumption异常时,立即展示当前测试已收集的示例 - 注册
finish_test事件:测试正常结束时记录示例;结合测试运行器的会话结束钩子,处理最后一个测试的GUI展示
示例代码框架:
from hypothesis import core import random class TestScenarioCollector: def __init__(self): self.current_test = None self.test_scenarios = {} self.NUM_VIZ = 5 def start_test(self, test): # 处理上一个测试的示例展示 if self.current_test and self.current_test in self.test_scenarios: self._show_scenarios(self.current_test) # 初始化当前测试的存储 self.current_test = test.__qualname__ self.test_scenarios.setdefault(self.current_test, []) def failed_test(self, test, exc_info): e = exc_info[1] if not isinstance(e, UnsatisfiedAssumption): self._show_scenarios(test.__qualname__) def add_scenario(self, scenario): if self.current_test# 问题背景与需求 针对Python Hypothesis测试,有以下需求: - 针对每个测试用例(如`def test_a(a: int)`、`def test_b(b: str)`),收集测试过程中使用的随机示例 - 单个测试的所有示例执行完毕后,随机抽取5个示例生成GUI窗口展示,阻塞至窗口关闭后再执行下一个测试 - 若测试中捕获到异常,立即基于已收集的示例弹出GUI # 现有实现的问题 目前通过`execute_example`方法实现,但存在明显缺陷:单个测试会多次触发GUI展示,且最后一个测试无法触发GUI。代码如下: ```python # It adds the scenario returned by the test into a collection of scenarios # to be used for a GUI demo, and displays some of the related scenarios whenever # the test changes or an exception occurs. def execute_example(self, fn): def viewScens(viewDesc): scens = self.scenarios[viewDesc] if scens: numScens = min(NUM_VIZ, len(scens)) print(f"showing {numScens} '{viewDesc}' scenarios") testGUI = TestGUI(random.sample(scens, numScens)) testGUI.run() self.curDesc = None self.curScen = None try: ret = fn() if self.curDesc != self.lastDesc: if self.curDesc in self.seenDescs: # PROBLEM: this happens several times per test print(f"'{self.curDesc}' has been seen before") # PROBLEM: this won't ever be called for the last test! viewScens(self.lastDesc) return ret except Exception as e: # UnsatisfiedAssumption is used to break out of an example where the preconditions aren't met if not isinstance(e, UnsatisfiedAssumption): print(f"exception with '{self.curDesc}'") viewScens(self.curDesc) raise e finally: self.seenDescs.add(self.curDesc) if isinstance(self.curScen, TestScenario): self.scenarios[self.curDesc].append(self.curScen) self.lastDesc = self.curDesc
待解决的问题
- 更优实现方案:如何正确实现需求,规避现有实现的问题?
- 测试后置操作方法:如果原需求无法直接实现,有没有办法在所有Hypothesis测试(或特定类/文件的测试)执行完毕后执行自定义操作?
- Hypothesis运行机制疑问:为什么Hypothesis会先运行一批
test_a的示例,重置状态后再运行更多test_a的示例?即使只有一个测试,也会多次检测到相同desc(意味着测试类被多次初始化),这个机制的运行逻辑是什么?
内容的提问来源于stack exchange,提问作者Nick Collins
相关产品推荐
相关产品推荐

