Skip to content

学生管理系统单元测试实战


课程目标

  • 掌握 Pytest 编写用例的结构与断言
  • 掌握 Pytest 自动测试实战能力
  • 熟悉 Pytest 参数化与基本装饰器用法
  • 掌握 Pytest 测试用例调度与运行
  • 掌握 Allure 报告生成


知识点总览

点击查看:自动化测试框架知识点梳理


需求说明

被测对象

  • 学生管理系统:
    • 随着学校的规模变大,对应的学员回越来越多,相应的管理越来越难。
    • 学员信息管理系统主要是对学员的各种信息进行管理,能够让学员的信息关系变得科学化、系统化和规范化。

测试需求

  • 测试学生管理系统的添加、修改、查询和删除功能。
  • 使用 pytest 编写自动化测试用例。
  • 输出 allure 测试报告。

实战思路

uml diagram


被测代码分析

# 定义一个学生类
class Student:

    def __init__(self, sid, name, age, gender):
        self.sid = sid
        self.name = name
        self.age = age
        self.gender = gender

    # 重写对象的显示格式 方法
    def __str__(self):
        return f"SID: {self.sid} --- Name: {self.name} --- Age: {self.age} --- Gender: {self.gender}"


# 封装管理类
class StudentManagement:

    def __init__(self):
        # 定义一个全局变量,用来保存学生的信息,方法各个方法之间进行访问
        self.students = []

    # 菜单方法
    def __menu(self):
        print("****************************************")
        print("*                学生管理系统           *")
        print("*        1. 添加新学生信息              *")
        print("*        2. 通过学号修改学生信息        *")
        print("*        3. 通过学号删除学生信息        *")
        print("*        4. 通过姓名删除学生信息        *")
        print("*        5. 通过学号查询学生信息          *")
        print("*        6. 通过姓名查询学生信息          *")
        print("*        7. 显示所有学生信息             *")
        print("*        8. 退出系统                  *")
        print("****************************************")
        select_op = input("输入编号选择操作:")
        return select_op

    # 获取学号
    def __get_sid(self):
        sid = input("请输入学生ID:")
        return sid

    # 获取姓名
    def __get_name(self):
        name = input("请输入学生姓名:")
        return name

    # 获取年龄
    def __get_age(self):
        while True:
            age = input("请输入学生年龄:")
            if age.isdigit():
                return int(age)
            else:
                print("输入年龄不合法,请输入数字")

    # 获取性别
    def __get_gender(self):
        gender = input("请输入学生性别:")
        return gender

    # 添加学生
    def add_student(self, sid, name, age, gender):
        for s in self.students:
            if s.sid == sid:
                print("学号已存在,添加失败")
                return "添加失败"
        else:
            student = Student(sid, name, age, gender)
            self.students.append(student)
            print("添加学生信息成功")
            return '添加成功'

    # 通过学号修改学生信息
    def modify_student_by_id(self, sid, name, age, gender):
        for s in self.students:
            if s.sid == sid:
                s.name = name
                s.age = age
                s.gender = gender
                print("修改成功")
                return "修改成功"
        else:
            print(f'没有 {sid} 对应的学生信息')
            return "修改失败"

    # 通过ID删除学生信息
    def delete_student_by_id(self, sid):
        for s in self.students:
            if s.sid == sid:
                self.students.remove(s)
                print("删除成功")
                return "删除成功"
        else:
            print(f'没有 {sid} 对应的学生信息')
            return "删除失败"

    # 通过学生姓名 删除所有符合的学生
    def delete_student_by_name(self, name):
        exist_s = []
        # 找出所有要删除的学生
        for s in self.students:
            if s.name == name:
                exist_s.append(s)

        # 开始删除
        if len(exist_s) > 0:
            for s in exist_s:
                self.students.remove(s)
                print(f"姓名为 { name } 的学生删除成功")
            else:
                print(f"成功删除 {len(exist_s)} 个学生")
                return "删除成功"
        else:
            print("学号不存在,无法删除")
            return "删除失败"

    # 通过学号查询学生信息
    def query_student_by_id(self, sid):
        for s in self.students:
            if s.sid == sid:
                print(f"学号 {sid} 的学生信息如下:")
                print(s)
                return "查询成功"
        else:
            print(f"学号 {sid} 的学生不存在")
            return "查询失败"

    # 通过姓名查询学生信息
    def query_student_by_name(self, name):
        result = []
        for s in self.students:
            if s.name == name:
                result.append(s)

        if len(result) > 0:
            print(f"姓名为 {name} 的学生共 {len(result)} 名,信息如下:")
            for s in result:
                print(s)
            return "查询成功"
        else:
            print(f"姓名为 {name} 的学生不存在")
            return "查询失败"

    # 显示所有学生信息
    def __show(self):
        print("所有学生信息如下:")
        for s in self.students:
            print(s)

    # 管理方法
    def manager(self):
        while True:
            select_op = self.__menu()
            if len(select_op) == 1 and select_op in "12345678":
                if select_op == "1":
                    sid = self.__get_sid()
                    name = self.__get_name()
                    age = self.__get_age()
                    gender = self.__get_gender()
                    self.add_student(sid, name, age, gender)
                elif select_op =="2":
                    sid = self.__get_sid()
                    name = self.__get_name()
                    age = self.__get_age()
                    gender = self.__get_gender()
                    self.modify_student_by_id(sid, name, age, gender)
                elif select_op =="3":
                    sid = self.__get_sid()
                    self.delete_student_by_id(sid)
                elif select_op =="4":
                    name = self.__get_name()
                    self.delete_student_by_name(name)
                elif select_op =="5":
                    sid = self.__get_sid()
                    self.query_student_by_id(sid)
                elif select_op =="6":
                    name = self.__get_name()
                    self.query_student_by_name(name)
                elif select_op =="7":
                    self.__show()
                else:
                    break
            else:
                print("输入的数据不合法,请输入在合法范围内的操作编号!!!")


# 程序入口
if __name__ == '__main__':
    StudentManagement().manager()

自动化测试用例编写

对以下方法完成冒烟测试:

  • 添加学生:sid, name, age, gender
  • 通过学号修改学生信息:sid, name, age, gender
  • 通过学号查询学生信息:sid
  • 通过姓名查询学生信息:name
  • 通过 ID 删除学生信息:sid
  • 通过学生姓名 删除所有符合的学生:name

# test_student_management.py

class TestStudentManagement:

    def setup_class(self):
        self.sm = StudentManagement()

    # 添加学生
    def test_add_student(self):
        result = self.sm.add_student('s01', "tom", 22, "male")
        assert result == "添加成功"

    # 通过学号修改学生信息
    def test_modify_student_byid(self):
        result = self.sm.modify_student_by_id('s01', "jack", 22, "male")
        assert result == "修改成功"

    # 通过学号查询学生信息
    def test_query_student_byid(self):
        result = self.sm.query_student_by_id('s01')
        assert result == "查询成功"

    # 通过姓名查询学生信息
    def test_query_student_byname(self):
        result = self.sm.query_student_by_name("tom")
        assert result == "查询成功"

    # 通过 ID 删除学生信息
    def test_delete_student_byid(self):
        result = self.sm.delete_student_by_id('s01')
        assert result == "删除成功"

    # 通过学生姓名 删除所有符合的学生
    def test_delete_student_byname(self):
        self.sm.add_student('s01', "tom", 22, "male")
        result = self.sm.delete_student_by_name("tom")
        assert result == "删除成功"

运行测试用例

# 运行当前路径下所有的测试用例
pytest

# 运行时打印详细日志与控制台输出结果
pytest -vs

# 运行指定测试文件中的某一个测试用例
pytest -vs test_student_management.py::TestStudentManagement::test_add_student

添加标签

为用例添加优先级标签。

class TestStudentManagement:

    def setup_class(self):
        self.sm = StudentManagement()

    # 添加学生
    @pytest.mark.P0
    def test_add_student(self):
        result = self.sm.add_student('s01', "tom", 22, "male")
        assert result == "添加成功"

    # 通过学号修改学生信息
    @pytest.mark.P0
    def test_modify_student_byid(self):
        result = self.sm.modify_student_by_id('s01', "jack", 22, "male")
        assert result == "修改成功"

    # 通过学号查询学生信息
    @pytest.mark.P0
    def test_query_student_byid(self):
        result = self.sm.query_student_by_id('s01')
        assert result == "查询成功"

    # 通过姓名查询学生信息
    def test_query_student_byname(self):
        result = self.sm.query_student_by_name("tom")
        assert result == "查询成功"

    # 通过 ID 删除学生信息
    @pytest.mark.P0
    def test_delete_student_byid(self):
        result = self.sm.delete_student_by_id('s01')
        assert result == "删除成功"

    # 通过学生姓名 删除所有符合的学生
    @pytest.mark.P1
    def test_delete_student_byname(self):
        self.sm.add_student('s01', "tom", 22, "male")
        result = self.sm.delete_student_by_name("tom")
        assert result == "删除成功"

运行指定标签的用例

# 只运行标签为 PO 的用例
pytest -vs -m P0

为自定的标签添加配置,添加 pytest.ini 文件

[pytest]
markers = P0
          P1

参数化

对添加学生功能完成测试

# test_add_student.py

class TestAddStudent:

    def setup_class(self):
        self.sm = StudentManagement()

    @pytest.mark.parametrize(
        "sid, name, age, gender",
        [
            ["s01", "tom", 22, "male"],
            ["s02", "jack", 30, "male"],
            ["s03", "lily", 18, "female"],
            ["s04", "ema", 16, "female"]
        ],
        ids=["add s01", "add s02", "add s03", "add s04"]
    )
    @pytest.mark.P1
    def test_add_student_byparams(self, sid, name, age, gender):
        '''
        参数化添加学生
        '''
        result = self.sm.add_student(sid, name, age, gender)
        assert result == "添加成功"

    @pytest.mark.P1
    def test_add_student_abnormal(self):
        '''
        重复 id 添加学生失败
        '''
        stu_info = ("s01", "tom", 22, "male")
        self.sm.add_student(*stu_info)
        result = self.sm.add_student(*stu_info)
        assert result == "添加失败"

控制用例顺序

pip install pytest-ordering

添加顺序标记

class TestStudentManagement:

    def setup_class(self):
        self.sm = StudentManagement()

    # 添加学生
    @pytest.mark.P0
    @pytest.mark.run(order=1)
    def test_add_student(self):
        result = self.sm.add_student('s01', "tom", 22, "male")
        assert result == "添加成功"

    # 通过学号修改学生信息
    @pytest.mark.P0
    def test_modify_student_byid(self):
        result = self.sm.modify_student_by_id('s01', "jack", 22, "male")
        assert result == "修改成功"

    # 通过学号查询学生信息
    @pytest.mark.P0
    @pytest.mark.run(order=3)
    def test_query_student_byid(self):
        result = self.sm.query_student_by_id('s01')
        assert result == "查询成功"

    # 通过姓名查询学生信息
    @pytest.mark.run(order=2)
    def test_query_student_byname(self):
        result = self.sm.query_student_by_name("tom")
        assert result == "查询成功"

分布式并发执行测试用例

pip install pytest-xdist

指定执行的线程数

pytest -vs -n 3

数据驱动

准备 yaml 格式测试数据

# stu_info.yaml

add:
  P1:
    data:
      - ["s05", "tom", 22, "male"]
      - ["s06", "jack", 30, "male"]
      - ["s07", "lily", 18, "female"]
      - ["s08", "ema", 16, "female"]
    ids:
      - add s05
      - add s06
      - add s07
      - add s08

创建 utils 工具

# utils/util.py

import yaml

class Utils:

    # 获取 yaml 数据
    @classmethod
    def get_yaml_data(cls, file_path, name, level):
        with open(file_path, encoding="utf-8") as f:
            # safe_load() 将 yaml 格式转成 python 对象
            result = yaml.safe_load(f)
            print(f"yaml 文件读取结果为 {result}")
        # 测试数据
        data = result.get(name).get(level).get('data')
        # 测试用例别名
        ids = result.get(name).get(level).get('ids')
        print(f"测试数据:{data}, 测试用例别名:{ids}")
        return data, ids

使用数据驱动方式完成添加学生功能测试

stu_info = Utils.get_yaml_data("./datas/stu_info.yaml", "add", "P1")

class TestAddStudent:

    def setup_class(self):
        self.sm = StudentManagement()

    @pytest.mark.parametrize(
        "sid, name, age, gender", stu_info[0], ids=stu_info[1]
    )
    def test_add_student_byyaml(self, sid, name, age, gender):
        '''
        数据驱动添加学生
        '''
        result = self.sm.add_student(sid, name, age, gender)
        assert result == "添加成功"

生成测试报告

添加 allure 描述

@allure.feature("学生管理系统")
class TestAddStudent:

    def setup_class(self):
        self.sm = StudentManagement()

    @pytest.mark.parametrize(
        "sid, name, age, gender",
        [
            ["s01", "tom", 22, "male"],
            ["s02", "jack", 30, "male"],
            ["s03", "lily", 18, "female"],
            ["s04", "ema", 16, "female"]
        ],
        ids=["add s01", "add s02", "add s03", "add s04"]
    )
    @pytest.mark.P1
    @allure.story("添加学生")
    @allure.title("参数化添加学生 {sid}, {name}")
    def test_add_student_byparams(self, sid, name, age, gender):
        '''
        参数化添加学生
        '''
        with allure.step("添加学生,获取添加结果"):
            result = self.sm.add_student(sid, name, age, gender)
        assert result == "添加成功"

生成 allure 报告

# 执行用例,搜集执行结果
pytest -v --alluredir=./result --clean-alluredir

# 生成在线 allure 报告
allure serve ./result

# 生成静态 allure 报告
allure generate --clean alluredir result -o result/html

# 打开静态报告
allure open -h 127.0.0.1 -p 8883 ./result/html

在配置文件 pytest.ini 中添加运行参数。配置完毕后,搜集结果直接执行 pytest 即可。

[pytest]
markers = P0
          P1
addopts = -v --alluredir=./result --clean-alluredir

使用 fixture

使用 fixture 管理生命周期,将类的实例化过程移到 fixture 中。

# conftest.py
@pytest.fixture(scope="class")
def sm():
    sm = StudentManagement()
    yield sm


# test_student_management.py
@allure.feature("学生管理系统")
class TestAddStudent:

    @pytest.mark.parametrize(
        "sid, name, age, gender",
        [
            ["s01", "tom", 22, "male"],
            ["s02", "jack", 30, "male"],
            ["s03", "lily", 18, "female"],
            ["s04", "ema", 16, "female"]
        ],
        ids=["add s01", "add s02", "add s03", "add s04"]
    )
    @pytest.mark.P1
    @allure.story("添加学生")
    @allure.title("参数化添加学生 {sid}, {name}")
    def test_add_student_byparams(self, sm, sid, name, age, gender):
        '''
        参数化添加学生
        '''
        with allure.step("添加学生,获取添加结果"):
            result = sm.add_student(sid, name, age, gender)
        assert result == "添加成功"

数据的清理

# conftest.py
@pytest.fixture()
def add_for_test(sm):
    sid = "for_test1"
    sm.add_student(sid, "for_test", 22, "male")
    yield sid
    sm.delete_student_by_id(sid)

# test_student_management.py
@allure.feature("学生管理系统")
class TestAddStudent:

    @pytest.mark.parametrize(
        "name, age, gender",
        [
            ["tom", 22, "male"],
            ["jack", 30, "male"],
            ["lily", 18, "female"],
            ["ema", 16, "female"]
        ],
        ids=["modify tom", "modify jack", "modify lily", "modify ema"]
    )
    @pytest.mark.P1
    @allure.story("修改学员信息")
    @allure.title("参数化添加学生 {sid}, {name}")
    # 通过学号修改学生信息
    def test_modify_student_byid(self, sm, add_for_test, name, age, gender):
        result = sm.modify_student_by_id(add_for_test, name, age, gender)
        assert result == "修改成功"
        assert sm.query_student_by_name(name) == "查询成功"


总结

  • Pytest 编写用例的结构与断言
  • Pytest 参数化与基本装饰器用法
  • Pytest 测试用例调度与运行
  • Allure 报告生成