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 QiskitTestCase / Operator 等价 Cirq cirq.testing 线路语义校验 PennyLane qml.assert_equal / 梯度校验 提交线路 ② 执行后端层(选择与配置) 模拟器后端(默认首选) AerSimulator / cirq.Simulator / default.qubit shots 可设、噪声可注入、结果可复现 真实硬件后端(验收用) IBM Quantum / 离子阱 / 超导真机 需排队、有噪声、结果带统计波动 返回 counts / 态矢量 ③ 结果校验层(把概率变成判定) 态矢量比对 保真度 ≥ 阈值 测量分布比对 TV 距离 / 卡方 期望值比对 误差棒内一致 梯度校验 解析 vs 差分

二、模拟器后端:单元测试的默认战场

单元测试追求快、稳、可复现,因此模拟器是默认后端。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 数推导容差,或改用保真度、总变差距离等连续指标并设定阈值。

综上,量子单元测试环境的搭建可以概括为"框架选型定断言风格、后端选型定执行成本、容差设计定判定可靠性"三件事。把模拟器作为回归主力、真机作为验收手段,并让两者共享线路构造与断言框架,是当前工程实践中较为稳妥的组织方式。