如何使用pytest测试PyQt5视频转文字应用的类方法?
PyQt5视频转文字项目的UI测试困惑
我是Python新手,正在开发一个基于PyQt5的视频转文字语音识别项目。之前用pytest测试过带返回值的普通函数,但现在不知道怎么测试这个项目里UI类的各种方法。已经把目标方法导入测试文件了,但搞不清要测什么、怎么测。目前写了测试主窗口构造函数和非MP4文件选择场景的测试用例,具体代码如下:
项目核心代码
#Imported modules import sys import wave, contextlib, math, time import speech_recognition as sr from moviepy.editor import AudioFileClip from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import QFileDialog, QMessageBox from PyQt5.QtCore import QThread, pyqtSignal class Ui_MainWindow(object): """Main window GUI.""" def __init__(self): """Initialisation function.""" self.mp4_file_name = "" self.output_file = "" self.audio_file = "speech.wav" def setupUi(self, MainWindow): """Define visual components and positions.""" # Main window MainWindow.setObjectName("MainWindow") MainWindow.resize(653, 836) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(50, 20, 161, 41)) # Selected video file label font = QtGui.QFont() font.setPointSize(14) self.label.setFont(font) self.label.setObjectName("label") self.selected_video_label = QtWidgets.QLabel(self.centralwidget) self.selected_video_label.setGeometry(QtCore.QRect(230, 20, 371, 41)) font = QtGui.QFont() font.setPointSize(8) self.selected_video_label.setFont(font) self.selected_video_label.setFrameShape(QtWidgets.QFrame.Box) self.selected_video_label.setText("") self.selected_video_label.setObjectName("selected_video_label") self.label_3 = QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(50, 90, 161, 41)) # Transcribed text box font = QtGui.QFont() font.setPointSize(14) self.label_3.setFont(font) self.label_3.setObjectName("label_3") self.transcribed_text = QtWidgets.QTextBrowser(self.centralwidget) self.transcribed_text.setGeometry(QtCore.QRect(230, 320, 381, 431)) self.transcribed_text.setObjectName("transcribed_text") self.label_5 = QtWidgets.QLabel(self.centralwidget) self.label_5.setGeometry(QtCore.QRect(230, 280, 161, 41)) font = QtGui.QFont() font.setPointSize(14) self.label_5.setFont(font) self.label_5.setObjectName("label_5") self.transcribe_button = QtWidgets.QPushButton(self.centralwidget) self.transcribe_button.setEnabled(False) self.transcribe_button.setGeometry(QtCore.QRect(230, 150, 221, 81)) # Transcribe button font = QtGui.QFont() font.setPointSize(14) self.transcribe_button.setFont(font) self.transcribe_button.setObjectName("transcribe_button") self.transcribe_button.clicked.connect(self.process_and_transcribe_audio) # progeress bar self.progress_bar = QtWidgets.QProgressBar(self.centralwidget) self.progress_bar.setGeometry(QtCore.QRect(230, 250, 381, 23)) self.progress_bar.setProperty("value", 0) self.progress_bar.setObjectName("progress_bar") self.message_label = QtWidgets.QLabel(self.centralwidget) self.message_label.setGeometry(QtCore.QRect(0, 760, 651, 21)) # Message label (for errors and warnings) font = QtGui.QFont() font.setPointSize(8) self.message_label.setFont(font) self.message_label.setFrameShape(QtWidgets.QFrame.Box) self.message_label.setText("") self.message_label.setObjectName("message_label") self.output_file_name = QtWidgets.QPlainTextEdit(self.centralwidget) self.output_file_name.setGeometry(QtCore.QRect(230, 90, 371, 41)) # Output file name font = QtGui.QFont() font.setPointSize(14) self.output_file_name.setFont(font) self.output_file_name.setObjectName("output_file_name") # Menubar options MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 653, 21)) self.menubar.setObjectName("menubar") self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName("menuFile") self.menuAbout = QtWidgets.QMenu(self.menubar) self.menuAbout.setObjectName("menuAbout") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.actionOpen_mp4_video_recording = QtWidgets.QAction(MainWindow) self.actionOpen_mp4_video_recording.setObjectName("actionOpen_mp4_video_recording") self.actionOpen_mp4_video_recording.triggered.connect(self.open_audio_file) self.actionAbout_vid2text = QtWidgets.QAction(MainWindow) self.actionAbout_vid2text.setObjectName("actionAbout_vid2text") self.actionAbout_vid2text.triggered.connect(self.show_about) self.actionNew = QtWidgets.QAction(MainWindow) self.actionNew.setObjectName("actionNew") self.actionNew.triggered.connect(self.new_project) self.menuFile.addAction(self.actionOpen_mp4_video_recording) self.menuFile.addAction(self.actionNew) self.menuAbout.addAction(self.actionAbout_vid2text) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuAbout.menuAction()) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): """Translate UI method.""" _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) self.label.setText(_translate("MainWindow", "Selected video file:")) self.label_3.setText(_translate("MainWindow", "Output file name:")) self.label_5.setText(_translate("MainWindow", "Transcribed text:")) self.transcribe_button.setText(_translate("MainWindow", "Transcribe")) self.output_file_name.setPlaceholderText(_translate("MainWindow", "interview1.txt")) self.menuFile.setTitle(_translate("MainWindow", "File")) self.menuAbout.setTitle(_translate("MainWindow", "About")) self.actionOpen_mp4_video_recording.setText(_translate("MainWindow", "Open mp4 video recording")) self.actionAbout_vid2text.setText(_translate("MainWindow", "About video to speech")) self.actionNew.setText(_translate("MainWindow", "New")) def open_audio_file(self): """Open the audio (*.mp4) file.""" file_name = QFileDialog.getOpenFileName() if file_name[0][-3:] == "mp4": self.transcribe_button.setEnabled(True) self.mp4_file_name = file_name[0] self.message_label.setText("") self.selected_video_label.setText(file_name[0]) else: self.message_label.setText("Please select an *.mp4 file") def convert_mp4_to_wav(self): """Convert the mp4 video file into an audio file.""" self.message_label.setText("Converting mp4 to audio (*.wav)...") self.convert_thread = convertVideoToAudioThread(self.mp4_file_name, self.audio_file) self.convert_thread.finished.connect(self.finished_converting) self.convert_thread.start() def get_audio_duration(self, audio_file): """Determine the length of the audio file.""" with contextlib.closing(wave.open(audio_file,'r')) as f: frames = f.getnframes() rate = f.getframerate() duration = frames / float(rate) return duration def transcribe_audio(self, audio_file): """Transcribe the audio file.""" total_duration = self.get_audio_duration(audio_file) / 10 total_duration = math.ceil(total_duration) self.td = total_duration if len(self.output_file_name.toPlainText()) > 0: self.output_file = self.output_file_name.toPlainText() else: self.output_file = "my_speech_file.txt" # Use thread to process in the background and avoid freezing the GUI self.thread = transcriptionThread(total_duration, audio_file, self.output_file) self.thread.finished.connect(self.finished_transcribing) self.thread.change_value.connect(self.set_progress_value) self.thread.start() def finished_converting(self): """Reset message text when conversion is finished.""" self.message_label.setText("Transcribing file...") self.transcribe_audio(self.audio_file) def finished_transcribing(self): """This run when transcription finished to tidy up UI.""" self.progress_bar.setValue(100) self.transcribe_button.setEnabled(True) self.message_label.setText("") self.update_text_output() def set_progress_value(self, val): """Update progress bar value.""" increment = int(math.floor(100*(float(val)/self.td))) self.progress_bar.setValue(increment) def process_and_transcribe_audio(self): """Process the audio into a textual transcription.""" self.transcribe_button.setEnabled(False) self.message_label.setText("Converting mp4 to audio (*.wav)...") self.convert_mp4_to_wav() def update_text_output(self): """Update the text box with the transcribed file.""" f = open(self.output_file, "r") self.transcribed_text.setText(f.read()) f.close() def new_project(self): """Clear existing fields of data.""" self.message_label.setText("") self.transcribed_text.setText("") self.selected_video_label.setText("") self.output_file_name.document().setPlainText("") self.progress_bar.setValue(0) def show_about(self): """Show about message box.""" msg = QMessageBox() msg.setWindowTitle("About video to Speech") msg.setText("Nelson Petro - CSE 111") msg.setIcon(QMessageBox.Information) msg.exec_() class convertVideoToAudioThread(QThread): """Thread to convert mp4 video file to wav file.""" def __init__(self, mp4_file_name, audio_file): """Initialization function.""" QThread.__init__(self) self.mp4_file_name = mp4_file_name self.audio_file = audio_file def __del__(self): """Destructor.""" self.wait() def run(self): """Run video conversion task.""" audio_clip = AudioFileClip(self.mp4_file_name) audio_clip.write_audiofile(self.audio_file) class transcriptionThread(QThread): """Thread to transcribe file from audio to text.""" change_value = pyqtSignal(int) def __init__(self, total_duration, audio_file, output_file): """Initialization function.""" QThread.__init__(self) self.total_duration = total_duration self.audio_file = audio_file self.output_file = output_file def __del__(self): """Destructor.""" self.wait() def run(self): """Run transcription, audio to text.""" r = sr.Recognizer() for i in range(0, self.total_duration): try: with sr.AudioFile(self.audio_file) as source: audio = r.record(source, offset=i*10, duration=10) f = open(self.output_file, "a") f.write(r.recognize_google(audio)) f.write(" ") self.change_value.emit(i) except: print("Unknown word detected...") continue f.close() def main(): app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_()) if __name__ == "__main__": main()
已编写的测试代码
def test_MainWindowConstructor(): ui = Ui_MainWindow() assert ui.mp4_file_name == "" assert ui.output_file == "" assert ui.audio_file == "speech.wav" def test_that_providing_a_raw_file_is_rejected(): app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) ui.actionOpen_mp4_video_recording.triggered QFileDialog.getOpenFileName = "video.raw" ui.open_audio_file assert ui.message_label.text == "Please select an *.mp4 file"
UI测试修正与扩展建议
1. 修复现有测试的问题
你写的第二个测试存在几个关键错误:
- 没有调用方法:
ui.open_audio_file只是引用方法,需要加()执行 - 错误修改
QFileDialog.getOpenFileName:这是类方法,不能直接赋值字符串,需要用mock模拟返回值 - 错误访问控件属性:
ui.message_label.text是方法,要写成ui.message_label.text()
修正后的测试(使用pytest-mock模拟Qt组件):
import pytest from PyQt5.QtWidgets import QApplication, QMainWindow from your_module import Ui_MainWindow # 替换为你的实际模块名 def test_MainWindowConstructor(): ui = Ui_MainWindow() assert ui.mp4_file_name == "" assert ui.output_file == "" assert ui.audio_file == "speech.wav" def test_that_providing_a_raw_file_is_rejected(mocker): app = QApplication([]) MainWindow = QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) # 模拟文件选择对话框返回非MP4文件 mocker.patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=("video.raw", "")) ui.open_audio_file() # 执行方法 assert ui.message_label.text() == "Please select an *.mp4 file" assert ui.transcribe_button.isEnabled() is False # 额外验证按钮状态 def test_valid_mp4_file_selection(mocker): app = QApplication([]) MainWindow = QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) test_file = "test_video.mp4" mocker.patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=(test_file, "")) ui.open_audio_file() assert ui.mp4_file_name == test_file assert ui.selected_video_label.text() == test_file assert ui.message_label.text() == "" assert ui.transcribe_button.isEnabled() is True
2. 可扩展的测试场景
针对你的项目,还可以覆盖这些测试点:
- new_project方法:验证所有UI元素是否被清空(文本框、进度条、标签等)
- update_text_output方法:临时创建测试输出文件,验证文本框是否正确加载内容
- 进度条计算:测试
set_progress_value是否根据输入值正确更新进度条 - 线程信号触发:模拟线程的
finished信号,验证后续UI更新逻辑是否执行
3. Qt UI测试注意事项
- 用
pytest-mock或unittest.mock模拟对话框、文件读写、外部API调用,避免测试依赖真实环境 - 测试时无需显示窗口,避免弹出UI干扰测试流程
- 线程相关逻辑重点测试信号连接后的UI行为,线程内部的业务逻辑可以单独抽离测试
内容的提问来源于stack exchange,提问作者Nelson Petro
相关产品推荐
相关产品推荐

