4. 量子程序单元测试环境搭建
量子程序的单元测试与经典软件测试最大的不同在于:被测对象不是一段确定性函数,而是一条会经历叠加、纠缠、测量坍缩的量子线路。因此,测试环境必须同时解决三件事——用什么框架写测试、在哪个后端上跑、如何把"概率性结果"变成可判定的断言。本节围绕这三件事,梳理 Qiskit、Cirq、PennyLane 三条主流工具链的测试能力,并给出模拟器与真实硬件后端的选择与配置方法。
一、三条工具链的测试能力对照
三个框架都提供了"构造线路—执行—取结果"的完整链路,但测试侧重点不同。Qiskit 的 qiskit.test 与 QiskitTestCase 面向线路等价性、算子对易性等结构化断言;Cirq 的 cirq.testing 提供 assert_circuits_with_terminal_measurements_are_equivalent、assert_has_consistent_apply_unitary 等针对线路语义的强校验;PennyLane 则以 qml.assert_equal 和梯度校验(grad 与有限差分对比)见长,适合变分量子算法的单元测试。
二、模拟器后端:单元测试的默认战场
单元测试追求快、稳、可复现,因此模拟器是默认后端。Qiskit 中通过 AerSimulator 配置 shots 与噪声模型;Cirq 用 cirq.Simulator() 直接拿到态矢量;PennyLane 的 default.qubit 支持解析梯度。下面是一段可直接运行的 Qiskit 测试骨架,演示"构造—执行—断言"的完整闭环:
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit.quantum_info import Statevector, state_fidelity
import numpy as np
def build_bell():
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
return qc
def test_bell_state_fidelity():
qc = build_bell()
# 1) 态矢量层面:与理想 Bell 态比对
sv = Statevector(qc)
ideal = Statevector([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)])
assert state_fidelity(sv, ideal) > 0.9999
def test_bell_measurement_distribution():
qc = build_bell()
qc.measure_all()
sim = AerSimulator()
result = sim.run(qc, shots=4096, seed_simulator=42).result()
counts = result.get_counts()
# 2) 测量层面:只应出现 00 与 11,且比例接近 1:1
assert set(counts.keys()) == {"00", "11"}
p00 = counts["00"] / 4096
assert abs(p00 - 0.5) < 0.03
注意两个关键细节:其一,seed_simulator=42 固定随机种子,使测量类断言可复现;其二,阈值不能设成精确相等,而应依据 shots 数估算统计涨落。shots=4096 时,单侧标准差约为 √(0.25/4096)≈0.0078,取 3σ 约 0.023,因此 0.03 的容差是合理的。若把 shots 降到 100,同样容差就会频繁误报——这是量子测试与经典测试在"断言设计"上的核心差异。
三、真实硬件后端:验收而非回归
真实硬件不适合做高频回归测试:排队时间长、每次运行结果都不同、噪声会系统性偏移分布。它的合理定位是阶段性验收——在模拟器上全部通过后,用真机确认线路在含噪环境下仍保持可辨识的统计特征。配置真机时需关注三件事:后端选择(按量子比特数、连通性、错误率挑选)、transpile 的优化等级与初始布局、以及 shots 与读出误差缓解(readout error mitigation)的开启。
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
from qiskit import transpile
service = QiskitRuntimeService(channel="ibm_quantum")
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=5)
qc = build_bell(); qc.measure_all()
isa_qc = transpile(qc, backend=backend, optimization_level=1)
sampler = SamplerV2(mode=backend)
job = sampler.run([isa_qc], shots=8192)
counts = job.result()[0].data.meas.get_counts()
真机断言必须放宽:由于读出误差,01、10 会以百分之几的概率出现,因此断言应写成"00 与 11 合计占比 > 0.90,且两者差值 < 0.10",而不是要求集合严格等于 {00, 11}。同时应记录后端名称、作业 ID 与提交时间,便于复盘时区分"线路错误"与"设备漂移"。
四、后端选择的决策依据
把选择逻辑固化成一张判断表,可以避免团队里"有人用模拟器、有人用真机"导致的测试口径不一致。核心判断顺序是:先看测试目的(回归/验收),再看资源约束(时间/配额),最后看结果判定方式(精确/统计)。模拟器负责日常回归与调试,真机负责里程碑验收;两者共用同一套线路构造函数,只替换执行后端与断言容差,这样测试代码的维护成本最低。
conftest.py 或等价夹具,把后端选择参数化:--backend=sim 跑全量回归,--backend=hw 只跑标记为 @pytest.mark.hardware 的验收用例。这样同一份测试代码可在两种环境下复用,且不会因真机排队拖慢 CI。assert counts == expected_counts 做精确比对。量子测量的统计性决定了这种断言必然失败。正确做法是基于 shots 数推导容差,或改用保真度、总变差距离等连续指标并设定阈值。综上,量子单元测试环境的搭建可以概括为"框架选型定断言风格、后端选型定执行成本、容差设计定判定可靠性"三件事。把模拟器作为回归主力、真机作为验收手段,并让两者共享线路构造与断言框架,是当前工程实践中较为稳妥的组织方式。