软件工程作业——结对项目
成员
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操作将例子写入文件中。
为了减少耗时,这里使用了多线程技术,运行多个线程同时生成例子,且仅仅在最后才将例子写入文件。
优化后耗时7ms,耗时下降了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方法
生成一个数学表达式问题,包括随机生成的操作数和运算符,以及可选的括号。
点击查看代码
```plaintext
def 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方法
随机生成与操作数数量相匹配的运算符列表。
点击查看代码
```plaintext
def generate_symbols(self):
"""
随机生成运算符。
根据操作数数量,从操作符列表中随机选择运算符。
"""
self.symbols = [self.ran.choice(self.operators) for _ in range(self.total - 1)]
(3)generate_bracket方法
根据运算符类型和数量,随机决定是否添加括号并确定其位置。
点击查看代码
```plaintext
def 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方法
生成随机整数或分数作为操作数,并确保操作数不会导致非法计算(如减法结果非正)。
点击查看代码
```plaintext
def 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方法
验证操作数的有效性,确保减法运算不会导致非正数结果。
点击查看代码
```plaintext
def 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方法
根据生成的操作数和运算符,构建数学表达式的字符串形式,包括括号。
点击查看代码
```plaintext
def 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方法
计算表达式的结果,并确保结果为正数。如果结果为非正数,则抛出异常。
点击查看代码
```plaintext
def calculate_result(self):
"""
计算表达式的结果,并确保结果是正数。
:return: 计算后的表达式结果,使用 Fraction 确保分数表示。
"""
# 使用 eval 函数计算字符串形式的表达式结果,并将其转换为 Fraction 类型
result = Fraction(eval(self.string_ver)).limit_denominator(1024)
# 确保结果大于0
if 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 # 正确答题数加 1
else:
print(f"× | The correct answer is: {fraction_to_mixed(correct_answer)}")
wrong_answers += 1 # 错误答题数加 1
print(f"正确题目数:{correct_answers}\n错误题目数:{wrong_answers}")
else:
# 否则,提示生成已完成
print("生成已经完成!")
# 结束时间
end_time = time.time()
# 计算执行时间
elapsed_time = end_time - start_time
print(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 # 正确答案数加 1
correct_list.append(counter) # 将题号加入正确列表
else:
wrong_answers += 1 # 错误答案数加 1
wrong_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.n
else:
n = 10 # 如果未提供题目数量,则默认生成 10 道题目
if args.r: # 如果提供了最大数值参数
r = args.r
m = 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 = 100
n = 5000
answer_questions(r, n)
# 验证文件内容或性能(可能需要单独的性能测试工具)
def test_zero_questions(self):
# 测试生成0个题目
r = 10
n = 0
answer_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:
pass
with open(ExerciseTestfile_path, 'r', encoding='utf-8') as file:
self.ExerciseTestfile_content = file.readlines()
pass
with open(AnswerTestfile_path, 'r', encoding='utf-8') as file:
self.AnswerTestfile_content = file.readlines()
pass
def test_basic_function(self):
# 假设已经有两个文件:Exercise.txt 和 Answer.txt,包含正确和错误的答案
exercise_file = self.ExerciseTestfile_content
answer_file = self.AnswerTestfile_content
check_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:
pass
with open("E:\python project\PairItem\Four_Basic_Operations\\file\Answer_emptyTest.txt", 'w') as fp:
pass
check_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 返回 False
question = 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_answer
def 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) # 默认是 False
def 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.项目小结
本次作业第一次接触到双人合作的项目,做起来实际上和单人项目的感觉差不多,每个人都需要了解到整个程序时怎么运行的,不单单是完成各自的模块。但是对于结对项目,因为具体分工的远古,在具体的编码过程工作量相对减少了很多,这也是比较单人项目完成的效率更高。对于结对项目,我认为能较好的锻炼我们在队伍中的交流沟通能力,具体分工也要因人而异,两个的水平不一,擅长的领域也不尽相同,需要良好的沟通去发挥各自的优势,并且在合作的过程中两个人相互学习,两人的能力都能得到提升。但是由于第一次合作,只是较为基础的完成了任务,仍需要更多的交流和学习。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?