这个作业属于哪个课程 | 22级计科12班 |
---|---|
这个作业要求在哪 | 作业要求 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
成员
姓名 | 学号 | GitHub地址 |
---|---|---|
吕宏鸿 | 3122004446 | 结对项目 |
宋观瑞 | 3122004402 | 结对项目 |
1.PSP表格
PSP2.1 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | 10 | 5 |
* 估计这个任务需要多少时间 | 10 | 5 |
开发 | 1210 | 1110 |
* 需求分析 (包括学习新技术) | 100 | 150 |
* 生成设计文档 | 30 | 30 |
* 设计复审 | 10 | 10 |
* 代码规范 (为目前的开发制定合适的规范) | 30 | 20 |
* 具体设计 | 180 | 200 |
* 具体编码 | 500 | 300 |
* 代码复审 | 180 | 200 |
* 测试(自我测试,修改代码,提交修改) | 180 | 200 |
报告 | 180 | 100 |
* 测试报告 | 90 | 60 |
* 计算工作量 | 60 | 10 |
* 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1400 | 1215 |
2.效能分析
性能测试
这里使用PyCharm自带的profile进行性能测试,下图是未修改代码是的耗时,可以看到耗时最长的自建函数是 answer_question(耗时59ms)。通过检查代码发现是因为生成的例子很多,逐个生成耗时长,且每次生成例子都得进行I/O操作将例子写入文件中。
为了减少耗时,这里使用了多线程技术,运行多个线程同时生成例子,且仅仅在最后才将例子写入文件。
优化后耗时7ma,耗时下降了88%,算是一个巨大的提升了。
3.设计实现过程
下面是程序的总体运行流程图。
Question类:
generate_question: 生成一个数学表达式问题,包括随机生成的操作数和运算符。
下面是generate_question方法的调用关系流程图
generate_symbols: 随机生成运算符。
generate_bracket: 随机决定是否添加括号,并确定括号位置。
generate_rational_numbers: 生成操作数,随机决定每个操作数是整数或分数。
validate_rational_numbers: 验证操作数的有效性,确保减法运算不会导致非正数。
generate_string: 根据生成的操作数和运算符,构建数学表达式的字符串形式。
calculate_result: 计算表达式的结果,并确保结果是正数。
file_operations:
generate_single_question: 生成一个数学题目并返回题目及答案。
answer_questions: 利用多线程,生成并解答随机的数学题目,将题目和答案分别保存到文件中。
check_answer: 校对题目和答案文件,检查答案是否正确,并将结果写入文件。
utils:
fraction_to mixed: 将 Fraction 对象转换为带分数的字符串表示形式。
mixed_to_fraction: 将带分数的字符串表示转换为 Fraction 对象。
main:
get_arguments: 解析命令行参数,返回包含参数的命名空间对象。
run_cmd_mode: 运行命令行模式,通过解析参数并生成题目或检查答案。
run_gui_mode: 运行图形化界面模式,通过解析参数并生成题目或检查答案。
4.代码说明
代码说明。展示出项目关键代码,并解释思路与注释说明。(4分)
1.question类
设计用于生成包含随机操作数(整数或分数)、运算符(加、减、乘、除)以及可选括号的数学表达式,并计算其结果。
(1)question方法
生成一个数学表达式问题,包括随机生成的操作数和运算符,以及可选的括号。
点击查看代码
```plaintextdef generate_question(self, max_value, num=4):"""生成一个数学表达式问题,包括随机生成的操作数和运算符。:param max_value: 操作数的最大值。:param num: 操作数的最大数量,默认值为 4。"""self.total = self.ran.randint(2, num) # 随机生成操作数的数量,最少 2 个self.generate_symbols() # 生成运算符self.generate_bracket() # 生成括号self.generate_rational_numbers(max_value) # 生成操作数self.generate_string() # 生成表达式的字符串形式self.result = self.calculate_result() # 计算表达式结果
(2)generate_symbols方法
随机生成与操作数数量相匹配的运算符列表。
点击查看代码
```plaintextdef generate_symbols(self):"""随机生成运算符。根据操作数数量,从操作符列表中随机选择运算符。"""self.symbols = [self.ran.choice(self.operators) for _ in range(self.total - 1)]
(3)generate_bracket方法
根据运算符类型和数量,随机决定是否添加括号并确定其位置。
点击查看代码
```plaintextdef generate_bracket(self):"""随机决定是否添加括号,并确定括号位置。如果操作符数量大于1,且是加法或减法运算,随机添加括号。"""if len(self.symbols) > 1:# 当符号是加法或减法时,有一定几率在符号位置插入括号self.bracket = next((i for i in range(self.total - 1) if self.symbols[i] in ['+', '-'] and self.ran.choice([True, False])),-2)
(4)generate_rational_numbers方法
生成随机整数或分数作为操作数,并确保操作数不会导致非法计算(如减法结果非正)。
点击查看代码
```plaintextdef generate_rational_numbers(self, max_value):"""生成操作数,随机决定每个操作数是整数或分数。:param max_value: 操作数的最大值。"""self.rational_numbers = [# 随机决定操作数是整数还是分数self.ran.randint(1, max_value) if self.ran.choice([True, False])else Fraction(self.ran.randint(1, self.ran.randint(2, max_value) - 1), self.ran.randint(2, max_value))for _ in range(self.total)]self.validate_rational_numbers() # 验证生成的操作数是否有效
(5)validate_rational_numbers方法
验证操作数的有效性,确保减法运算不会导致非正数结果。
点击查看代码
```plaintextdef validate_rational_numbers(self):"""验证操作数的有效性,确保减法运算不会导致非正数。如果操作数导致结果为负数或零,抛出异常。"""for i in range(1, self.total):# 如果符号是减法,且前后两个操作数相减结果为 0 或负数,则抛出异常if self.symbols[i - 1] == '-' and self.rational_numbers[i - 1] - self.rational_numbers[i] <= 0:raise ValueError # 抛出异常以重新生成问题
(6)generate_string方法
根据生成的操作数和运算符,构建数学表达式的字符串形式,包括括号。
点击查看代码
```plaintextdef generate_string(self):"""根据生成的操作数和运算符,构建数学表达式的字符串形式。"""string_ver = "" # 初始化空字符串,用于存储表达式# 遍历操作数和运算符,构建表达式字符串for i in range(self.total - 1):if self.bracket == i:string_ver += '(' # 如果当前索引等于括号位置,添加左括号# 如果当前操作数是分数,将其包裹在括号中if isinstance(self.rational_numbers[i], Fraction):string_ver += '(' + str(self.rational_numbers[i]) + ')'else:string_ver += str(self.rational_numbers[i]) # 否则直接添加整数if self.bracket + 1 == i:string_ver += ')' # 添加右括号string_ver += self.symbols[i] # 添加运算符# 添加最后一个操作数if isinstance(self.rational_numbers[-1], Fraction):string_ver += '(' + str(self.rational_numbers[-1]) + ')'else:string_ver += str(self.rational_numbers[-1])# 如果括号位置在最后一个运算符处,添加右括号if self.bracket == i:string_ver += ')'# 将生成的表达式存储为字符串形式self.string_ver = string_ver
(7)calculate_result方法
计算表达式的结果,并确保结果为正数。如果结果为非正数,则抛出异常。
点击查看代码
```plaintextdef calculate_result(self):"""计算表达式的结果,并确保结果是正数。:return: 计算后的表达式结果,使用 Fraction 确保分数表示。"""# 使用 eval 函数计算字符串形式的表达式结果,并将其转换为 Fraction 类型result = Fraction(eval(self.string_ver)).limit_denominator(1024)# 确保结果大于0if result <= 0:raise ValueError # 如果结果不为正,抛出异常self.result = result # 存储计算结果return result # 返回结果
2.question函数
(1)answer_question函数
生成并解答随机的数学题目,将题目和答案分别保存到文件中。
点击查看代码
```plaintext
def answer_questions(r, n, answer_mode=False):"""生成并解答随机的数学题目,将题目和答案分别保存到文件中。:param r: 操作数的最大值:param n: 题目数量:param answer_mode: 是否启用用户答题模式,True 为启用答题模式"""correct_answers = 0 # 用于统计用户正确答题数wrong_answers = 0 # 用于统计用户错误答题数# 计时start_time = time.time()# 清空已有的题目和答案文件with open("file\\Exercise.txt", 'w', encoding="UTF-8"), open("file\\Answer.txt", 'w', encoding="UTF-8"):pass # 占位操作,目的是清空文件内容# 用于存储生成的题目和答案的列表question_data = []# 使用线程池并行生成题目if n > 5000:with concurrent.futures.ThreadPoolExecutor(max_workers=1024) as executor:futures = [executor.submit(generate_single_question, r, question_num) for question_num in range(1, n + 1)]# 查看当前活跃的线程数量print(f"当前活跃的线程数: {threading.active_count()}")for future in concurrent.futures.as_completed(futures):question_data.append(future.result())else:with concurrent.futures.ThreadPoolExecutor() as executor:futures = [executor.submit(generate_single_question, r, question_num) for question_num in range(1, n + 1)]# 查看当前活跃的线程数量print(f"当前活跃的线程数: {threading.active_count()}")for future in concurrent.futures.as_completed(futures):question_data.append(future.result())# 按题号对生成的题目和答案排序question_data.sort(key=lambda x: x[0])# 将生成的题目和答案写入文件中with open("file\\Exercise.txt", 'a', encoding="UTF-8") as fp1, open("file\\Answer.txt", 'a', encoding="UTF-8") as fp2:for _, question_text, answer_text in question_data:fp1.write(question_text)fp2.write(answer_text)# 如果启用了答题模式if answer_mode:for question_num, question_text, answer_text in question_data:answer = input(f"({question_num}){question_text.strip()} ")# 将用户输入的答案转换为 Fraction 类型answer = mixed_to_fraction(answer)# 解析正确答案correct_answer = mixed_to_fraction(answer_text.split(". ")[1].strip())# 判断用户答案是否正确if answer == correct_answer:print("√") # 如果正确,打印 √correct_answers += 1 # 正确答题数加 1else:print(f"× | The correct answer is: {fraction_to_mixed(correct_answer)}")wrong_answers += 1 # 错误答题数加 1print(f"正确题目数:{correct_answers}\n错误题目数:{wrong_answers}")else:# 否则,提示生成已完成print("生成已经完成!")# 结束时间end_time = time.time()# 计算执行时间elapsed_time = end_time - start_timeprint(f"耗时: {elapsed_time:.2f} s")
(2)check_answer函数
校对题目和答案文件,检查答案是否正确,并将结果写入文件。
点击查看代码
```plaintext
def check_answer(exercise_file, answer_file):"""校对题目和答案文件,检查答案是否正确,并将结果写入文件。:param exercise_file: 题目文件路径:param answer_file: 答案文件路径"""correct_answers = 0 # 正确答案数量wrong_answers = 0 # 错误答案数量correct_list = [] # 保存正确题目的编号wrong_list = [] # 保存错误题目的编号# 读取题目和答案文件的行数,以便计算总进度with open(exercise_file, 'r', encoding="UTF-8") as fp1, open(answer_file, 'r', encoding="UTF-8") as fp2:total_lines = sum(1 for _ in fp1) # 计算题目行数fp1.seek(0) # 重置文件指针到开头fp2.seek(0) # 重置文件指针到开头counter = 1 # 题目编号计数器exe_line = fp1.readline() # 读取一行题目ans_line = fp2.readline() # 读取一行答案while exe_line and ans_line:# 提取出题目中的表达式部分exe_line = exe_line[exe_line.index('. ') + 1:-3]# 提取出答案部分,并将其转换为 Fraction 类型ans_line = ans_line[ans_line.index('. ') + 1:]ans_line = str(mixed_to_fraction(ans_line))# 计算题目表达式的正确结果result_exe = Fraction(eval(exe_line)).limit_denominator(1024)# 计算答案的结果result_ans = Fraction(eval(ans_line)).limit_denominator(1024)# 比较答案是否正确if result_ans == result_exe:correct_answers += 1 # 正确答案数加 1correct_list.append(counter) # 将题号加入正确列表else:wrong_answers += 1 # 错误答案数加 1wrong_list.append(counter) # 将题号加入错误列表# 更新进度条print("\r", end="")progress = int((counter / total_lines) * 50) # 进度条长度为20个字符percentage = (counter / total_lines) * 100 # 计算百分比print("校对进度: {:.2f}%: ".format(percentage), "▋" * progress + " " * (50 - progress), end="")sys.stdout.flush()# 读取下一行题目和答案exe_line = fp1.readline()ans_line = fp2.readline()counter += 1 # 题号加 1# 将结果写入成绩文件with open("file\\Grade.txt", 'w', encoding="UTF-8") as fp:fp.write(f"Correct: {correct_answers} {tuple(correct_list)}\n") # 写入正确答案信息fp.write(f"Wrong: {wrong_answers} {tuple(wrong_list)}\n") # 写入错误答案信息print("输出结果已写入file文件夹中的Grade.txt")
3.utils函数
(1)fraction_to_mixed函数
功能是将一个 Fraction 对象转换为带分数的字符串表示形式。
点击查看代码
```plaintext
def fraction_to_mixed(frac: Fraction) -> str:"""将 Fraction 对象转换为带分数的字符串表示形式。:param frac: 需要转换的 Fraction 对象,表示分数。:return: 分数的字符串表示形式,如果是假分数,返回带分数格式,否则返回分数的原始字符串。- 如果分数的分子(numerator)小于或等于分母(denominator),或分母为 1,则直接返回该分数的字符串表示。- 如果是假分数(分子大于分母),则返回带分数的格式:例如,对于 7/3,返回 "2'1/3",即带分数形式:整数部分'分数部分。"""# 如果分子小于等于分母,或分母为1,返回分数原始形式if frac.numerator <= frac.denominator or frac.denominator == 1:return str(frac) # 直接返回 Fraction 的字符串表示# 对于假分数,返回带分数形式# 整数部分: frac.numerator // frac.denominator,余数部分为 frac - (整数部分)else:return f"{frac.numerator // frac.denominator}'{frac - frac.numerator // frac.denominator}"
(2)mixed_to_fraction函数
这个函数 mixed_to_fraction 的目的是将带分数的字符串表示(如 "2'1/3")或简单的分数字符串(如 "3/4")转换为 Fraction 对象。
点击查看代码
```plaintext
def mixed_to_fraction(string: str) -> Fraction:"""将带分数的字符串表示转换为 Fraction 对象。:param string: 带分数的字符串,例如 "2'1/3" 或简单的 "3/4"。:return: 对应的 Fraction 对象。- 如果字符串包含带分数形式(即包含 "'" 和 "/"),将其分为整数部分和分数部分,并组合成一个 Fraction 对象。- 如果是普通分数,直接将其转换为 Fraction 对象。"""# 如果字符串包含带分数的表示形式(即同时包含 "'" 和 "/")if "'" in string and '/' in string:whole, frac_part = string.split("'") # 将字符串分为整数部分和分数部分numerator, denominator = map(int, frac_part.split("/")) # 将分数部分解析为分子和分母# 返回整数部分 + 分数部分组成的 Fraction 对象return int(whole) + Fraction(numerator, denominator)# 如果字符串表示的是简单的分数形式,则直接转换为 Fraction 对象return Fraction(string)
4.main函数
点击查看代码
```plaintext
def get_arguments():"""解析命令行参数,返回包含参数的命名空间对象。参数包括生成题目的数量、最大数值、是否启用答题模式、题目文件路径、答案文件路径等。"""parser = argparse.ArgumentParser(description="Usage Example: python main.py -n 3 -r 10", # 描述程序用途epilog="若需要使用判题功能,请勿输入-r参数" # 在帮助信息末尾显示的额外信息)# 添加命令行参数parser.add_argument('-n', type=int, help="生成题目的个数(默认为10)")parser.add_argument('-r', type=int, help="题目中的最大数值")parser.add_argument('-m', action='store_true', help="启用答题模式(默认关闭)")parser.add_argument('-e', type=str, help="题目文件路径")parser.add_argument('-a', type=str, help="答案文件路径")return parser.parse_args() # 返回解析后的命名空间对象def run_cmd_mode():"""运行命令行模式,通过解析参数并生成题目或检查答案。"""args = get_arguments() # 获取命令行参数if args.n: # 如果提供了题目数量参数n = args.nelse:n = 10 # 如果未提供题目数量,则默认生成 10 道题目if args.r: # 如果提供了最大数值参数r = args.rm = args.m if args.m else False # 判断是否启用了答题模式,默认关闭answer_questions(r, n, m) # 调用生成题目的函数elif args.e and args.a: # 如果提供了题目文件路径和答案文件路径check_answer(args.e, args.a) # 调用检查答案的函数else:print("Argument '-r' is needed!") # 如果没有提供必要参数,输出错误信息os.system("pause") # 暂停程序等待用户输入,防止窗口立即关闭sys.exit() # 退出程序def run_gui_mode():"""运行图形化界面模式,用户可以通过界面输入生成题目或答案。"""def generate_questions():"""生成题目并将其显示在图形界面的文本框中,同时生成对应的答案。"""try:n = int(entry_n.get()) # 获取用户输入的题目数量r = int(entry_r.get()) # 获取用户输入的最大数值answer_mode = var_answer_mode.get() # 获取用户是否启用答题模式的选择# 清空文本框内容text_output.delete(1.0, tk.END)# 调用生成题目函数,并根据用户选择的模式生成题目或答案answer_questions(r, n, answer_mode)# 读取生成的题目和答案文件,并显示在文本框中with open("file/Exercise.txt", 'r', encoding="UTF-8") as f:text_output.insert(tk.END, f.read()) # 将生成的题目写入文本框with open("file/Answer.txt", 'r', encoding="UTF-8") as f:text_output.insert(tk.END, f"\n\nAnswers:\n{f.read()}") # 将生成的答案写入文本框except ValueError:messagebox.showerror("输入错误", "请确保输入的是有效的数字。") # 如果输入的不是有效数字,则弹出错误消息框# 创建主窗口root = tk.Tk()root.title("四则运算生成器") # 设置窗口标题root.geometry("600x400") # 设置窗口尺寸# 题目数量输入框label_n = tk.Label(root, text="题目数量 (n):")label_n.pack()entry_n = tk.Entry(root)entry_n.pack()# 最大数值输入框label_r = tk.Label(root, text="最大数值 (r):")label_r.pack()entry_r = tk.Entry(root)entry_r.pack()# 答题模式选项(复选框)var_answer_mode = tk.IntVar() # 用于存储复选框的值check_answer_mode = tk.Checkbutton(root, text="启用答题模式", variable=var_answer_mode) # 答题模式复选框check_answer_mode.pack()# 生成题目按钮button_generate = tk.Button(root, text="生成题目", command=generate_questions) # 点击按钮后调用生成题目的函数button_generate.pack()# 输出区域(用于显示生成的题目和答案)text_output = tk.Text(root, height=15, width=70)text_output.pack()# 启动图形化界面的主循环root.mainloop()if __name__ == '__main__':# 判断是否传入命令行参数,如果有参数,则运行命令行模式;否则运行图形化界面if len(sys.argv) > 1:run_cmd_mode() # 运行命令行模式else:run_gui_mode() # 启动图形化界面
5.运行测试
utils测试
测试包括函数能否将真分数、假分数、整数和字符串相互转换,以及函数在接受到无效输入时的行为
点击查看代码
```plaintext
class TestFractionConversion(unittest.TestCase):def test_fraction_to_mixed_proper_fraction(self):# 测试真分数frac = Fraction(1, 3)expected = "1/3"result = fraction_to_mixed(frac)self.assertEqual(result, expected, msg="Failed to convert proper fraction to string")def test_fraction_to_mixed_improper_fraction(self):# 测试假分数frac = Fraction(7, 3)expected = "2'1/3"result = fraction_to_mixed(frac)self.assertEqual(result, expected, msg="Failed to convert improper fraction to mixed string")def test_fraction_to_mixed_whole_number(self):# 测试整数(可以视为分母为1的分数)frac = Fraction(5, 1)expected = "5"result = fraction_to_mixed(frac)self.assertEqual(result, expected, msg="Failed to convert whole number fraction to string")def test_mixed_to_fraction_mixed_number(self):# 测试带分数mixed_str = "2'1/3"expected = Fraction(7, 3)result = mixed_to_fraction(mixed_str)self.assertEqual(result, expected, msg="Failed to convert mixed number string to Fraction")def test_mixed_to_fraction_simple_fraction(self):# 测试简单分数simple_str = "3/4"expected = Fraction(3, 4)result = mixed_to_fraction(simple_str)self.assertEqual(result, expected, msg="Failed to convert simple fraction string to Fraction")def test_mixed_to_fraction_invalid_input(self):# 测试无效输入(不包含 '/')invalid_str = "2'1"with self.assertRaises(ValueError, msg="Expected ValueError for invalid input"):mixed_to_fraction(invalid_str)def test_mixed_to_fraction_integer_input(self):# 测试整数输入(虽然不是带分数,但函数应该能够处理)integer_str = "5"expected = Fraction(5, 1)result = mixed_to_fraction(integer_str)self.assertEqual(result, expected, msg="Failed to convert integer string to Fraction")
file_operations测试
TestAnswerQuestions 类
主要用于测试函数是否能生成少量题目(以10为例)、生成大量题目(5000为例)、以及测试生成0个题目的边界情况
点击查看代码
```plaintext
class TestAnswerQuestions(unittest.TestCase):def test_basic_function(self):# 测试生成10个题目r = 10 # 操作数的最大值n = 10 # 题目数量answer_questions(r, n)# 这里可以添加检查文件内容的代码来验证是否正确生成了题目def test_large_number_of_questions(self):# 测试生成大量题目r = 100n = 5000answer_questions(r, n)# 验证文件内容或性能(可能需要单独的性能测试工具)def test_zero_questions(self):# 测试生成0个题目r = 10n = 0answer_questions(r, n)
TestCheckAnswer 类
用于测试 check_answer 函数,该函数负责比较题目文件和答案文件,并生成成绩文件。
点击查看代码
```plaintext
class TestCheckAnswer(unittest.TestCase):def setUp(self):ExerciseTestfile_path = "E:\python project\PairItem\Four_Basic_Operations\\file\ExerciseTest.txt"AnswerTestfile_path = "E:\python project\PairItem\Four_Basic_Operations\\file\AnswerTest.txt"grade_file_path = "E:\python project\PairItem\Four_Basic_Operations\\file\Grade.txt"# 使用'w'模式打开文件,这将清空文件内容(如果文件已存在)# 或者创建一个新文件(如果文件不存在)with open(grade_file_path, 'w') as grade_file:passwith open(ExerciseTestfile_path, 'r', encoding='utf-8') as file:self.ExerciseTestfile_content = file.readlines()passwith open(AnswerTestfile_path, 'r', encoding='utf-8') as file:self.AnswerTestfile_content = file.readlines()passdef test_basic_function(self):# 假设已经有两个文件:Exercise.txt 和 Answer.txt,包含正确和错误的答案exercise_file = self.ExerciseTestfile_contentanswer_file = self.AnswerTestfile_contentcheck_answer("E:\python project\PairItem\Four_Basic_Operations\\file\ExerciseTest.txt", "E:\python project\PairItem\Four_Basic_Operations\\file\AnswerTest.txt")# 验证Grade.txt文件的内容是否正确def test_empty_files(self):# 测试空文件with open("E:\python project\PairItem\Four_Basic_Operations\\file\Exercise_emptyTest.txt", 'w') as fp:passwith open("E:\python project\PairItem\Four_Basic_Operations\\file\Answer_emptyTest.txt", 'w') as fp:passcheck_answer("E:\python project\PairItem\Four_Basic_Operations\\file\Exercise_emptyTest.txt", "E:\python project\PairItem\Four_Basic_Operations\\file\Answer_emptyTest.txt")# 验证Grade.txt文件的内容(应该显示没有题目)Four_Basic_Operations
结果验证
由上面的测试题目和测试答案对比,可知这个测试用例成功
Question测试
TestQuestion 类
包含了一系列用于测试 Question 类的测试用例。这些测试用例覆盖了生成问题、符号、有理数、字符串表示、计算结果以及验证有理数等方面的功能。
点击查看代码
```plaintext
class TestQuestion(unittest.TestCase):@patch('question.Random') # 模拟 Random 类def test_generate_question(self, mock_random):# 初始化 Mock 随机数生成器的行为mock_random.return_value = Random(42) # 让随机数生成器有可预期的输出question = Question()question.generate_question(max_value=10, num=3)# 检查生成的运算符self.assertEqual(len(question.symbols), question.total - 1)# 检查操作数的个数是否正确self.assertEqual(len(question.rational_numbers), question.total)# 确保结果是 Fraction 类型并且结果为正数self.assertTrue(isinstance(question.result, Fraction))self.assertGreater(question.result, 0)def test_generate_symbols(self):question = Question(total=4)question.generate_symbols()# 确保 symbols 数量和 total - 1 是一致的self.assertEqual(len(question.symbols), 3)for symbol in question.symbols:self.assertIn(symbol, question.operators)@patch('random.Random.choice')def test_generate_bracket_no_symbols(self, mock_choice):# 测试当 symbols 列表长度小于等于1时,不应生成括号question = Question(total=2, symbols=[])question.generate_bracket()self.assertEqual(question.bracket, -2) # 确保没有生成括号@patch('random.Random.choice')def test_generate_bracket_with_addition(self, mock_choice):# 测试当 symbols 中包含加法运算时,括号生成的位置mock_choice.return_value = True # 强制使得 choice 返回 True,以便生成括号question = Question(total=3, symbols=['+', '*'])question.generate_bracket()# 确保括号生成在加法符号的位置self.assertEqual(question.bracket, 0)@patch('random.Random.choice')def test_generate_bracket_with_subtraction(self, mock_choice):# 测试当 symbols 中包含减法运算时,括号生成的位置mock_choice.return_value = True # 强制使得 choice 返回 True,以便生成括号question = Question(total=4, symbols=['*', '-'])question.generate_bracket()# 确保括号生成在减法符号的位置self.assertEqual(question.bracket, 1)@patch('random.Random.choice')def test_no_bracket_generated(self, mock_choice):# 测试当 Random.choice 返回 False 时,不应生成括号mock_choice.return_value = False # 强制 choice 返回 Falsequestion = Question(total=4, symbols=['+', '-'])question.generate_bracket()self.assertEqual(question.bracket, -2) # 确保没有生成括号def test_generate_rational_numbers(self):question = Question(total=3)try:question.generate_symbols() # 生成符号question.generate_rational_numbers(max_value=9)# 检查生成的操作数数量self.assertEqual(len(question.rational_numbers), 3)for num in question.rational_numbers:# 检查操作数是否为整数或 Fraction 类型self.assertTrue(isinstance(num, int) or isinstance(num, Fraction))except ValueError as e:self.fail(f"生成的操作数无效: {str(e)}")def test_generate_string(self):question = Question(total=3, rational_numbers=[1, Fraction(1, 2), 3], symbols=['+', '*'])question.generate_string()expected_string = "1+(1/2)*3"self.assertEqual(question.string_ver, expected_string)def test_calculate_result(self):question = Question(total=3, rational_numbers=[1, Fraction(1, 2), 3], symbols=['+', '*'])question.generate_string()result = question.calculate_result()# 检查计算结果self.assertEqual(result, Fraction(5, 2))def test_validate_rational_numbers(self):question = Question(total=3, rational_numbers=[5, 3, 2], symbols=['-', '+'])with self.assertRaises(ValueError):question.validate_rational_numbers() # 应该抛出 ValueError,因为 5 - 3 = 2, 3 - 2 = 1 是有效的
main测试
点击查看代码
```plaintext
class TestMainFunctions(unittest.TestCase):@patch('main_2.answer_questions') # Mock answer_questions@patch('main_2.check_answer') # Mock check_answerdef test_run_cmd_mode_generate_questions(self, mock_check_answer, mock_answer_questions):# 模拟命令行参数test_args = ['main.py', '-n', '5', '-r', '10']with patch.object(sys, 'argv', test_args):run_cmd_mode()mock_answer_questions.assert_called_once_with(10, 5, False)@patch('main_2.answer_questions')@patch('main_2.check_answer')def test_run_cmd_mode_check_answers(self, mock_check_answer, mock_answer_questions):# 模拟命令行参数test_args = ['main.py', '-e', 'questions.txt', '-a', 'answers.txt']with patch.object(sys, 'argv', test_args):run_cmd_mode()mock_check_answer.assert_called_once_with('questions.txt', 'answers.txt')@patch('builtins.print')@patch('os.system')def test_run_cmd_mode_missing_argument(self, mock_os_system, mock_print):# 模拟命令行参数缺失test_args = ['main.py']with patch.object(sys, 'argv', test_args):with self.assertRaises(SystemExit): # 期望程序退出run_cmd_mode()mock_print.assert_called_once_with("Argument '-r' is needed!") # 确保输出了错误信息mock_os_system.assert_called_once_with("pause") # 确保调用了暂停def test_get_arguments(self):# 测试命令行参数解析test_args = ['main.py', '-n', '5', '-r', '10']with patch.object(sys, 'argv', test_args):args = get_arguments()self.assertEqual(args.n, 5)self.assertEqual(args.r, 10)self.assertFalse(args.m) # 默认是 Falsedef test_get_arguments_invalid_input(self):# 测试命令行参数解析无效输入test_args = ['main.py', '-n', 'x', '-r', 'x']with patch.object(sys, 'argv', test_args):with self.assertRaises(SystemExit): # argparse 会调用 sys.exit()get_arguments()
6.项目小结
本次作业第一次接触到双人合作的项目,做起来实际上和单人项目的感觉差不多,每个人都需要了解到整个程序时怎么运行的,不单单是完成各自的模块。但是对于结对项目,因为具体分工的远古,在具体的编码过程工作量相对减少了很多,这也是比较单人项目完成的效率更高。对于结对项目,我认为能较好的锻炼我们在队伍中的交流沟通能力,具体分工也要因人而异,两个的水平不一,擅长的领域也不尽相同,需要良好的沟通去发挥各自的优势,并且在合作的过程中两个人相互学习,两人的能力都能得到提升。但是由于第一次合作,只是较为基础的完成了任务,仍需要更多的交流和学习。