软件工程作业——结对项目

这个作业属于哪个课程 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操作将例子写入文件中。

为了减少耗时,这里使用了多线程技术,运行多个线程同时生成例子,且仅仅在最后才将例子写入文件。

优化后耗时7ms,耗时下降了88%,算是一个巨大的提升了。

dad

3.设计实现过程

下面是程序的总体运行流程图。

1212

Question类:

generate_question: 生成一个数学表达式问题,包括随机生成的操作数和运算符。

下面是generate_question方法的调用关系流程图

dada

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.项目小结

本次作业第一次接触到双人合作的项目,做起来实际上和单人项目的感觉差不多,每个人都需要了解到整个程序时怎么运行的,不单单是完成各自的模块。但是对于结对项目,因为具体分工的远古,在具体的编码过程工作量相对减少了很多,这也是比较单人项目完成的效率更高。对于结对项目,我认为能较好的锻炼我们在队伍中的交流沟通能力,具体分工也要因人而异,两个的水平不一,擅长的领域也不尽相同,需要良好的沟通去发挥各自的优势,并且在合作的过程中两个人相互学习,两人的能力都能得到提升。但是由于第一次合作,只是较为基础的完成了任务,仍需要更多的交流和学习。

posted @   我是张继凯  阅读(78)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示