selenium_ui自动化测试_unittest

unittest 简介

unittest 是Python 内置单元测试框架,无需 pip 安装,是接口、UI 自动化常用框架。

核心作用:批量管理测试用例、执行用例、断言结果、统计成功 / 失败 / 错误数量。

核心

  1. TestCase(测试用例类)
  • 导入:import unittest
  • 必须继承 unittest.TestCase,一个类代表一组测试场景。
  • 类中只有以 test_ 开头的方法,才会被识别为测试用例
  • 测试方法名建议见名知意
  1. Fixture 前置后置(4个核心方法)
  • Fixture:测试执行前后,自动运行的准备、清理代码(比如打开浏览器、关闭浏览器、进入页面)

Fixture 就是测试的前置准备、后置清理的一套操作,准备测试环境,测试结束后回收环境。

执行顺序:
setUpClass(类开始,只执行 1 次)
→ setUp(每条用例前执行)
→ test_xxx(测试用例)
→ tearDown(每条用例后执行)
→ setUp → test_xxx → tearDown
→ tearDownClass(全部用例跑完,最后执行 1 次)

方法 装饰器 参数 作用
setUpClass @classmethod cls 整个测试类只执行一次,全局初始化(打开浏览器一次)
setUp 无 self 每条用例执行前执行,每条用例独立初始化
tearDown 无 self 每条用例结束后执行,清理(关闭浏览器)
tearDownClass @classmethod cls 全部用例跑完,执行一次,全局清理

两种使用模式
模式 1:setUp 打开浏览器,tearDown 关闭浏览器。每条用例独立浏览器,用例隔离,失败互不影响,速度慢(你当前在用)
模式 2:setUpClass 打开浏览器,tearDownClass 关闭。全程共用浏览器,速度快;但用例互相污染

  1. 断言assert(判断用例是否通过⭐️)
  • 用来判断实际结果和预期是否一致,断言失败 → 用例标记失败
  • self.assertEqual(实际值,预期值) 相等
  • self.assertTrue(表达式) 表达式结果为 True
  • self.assertIn(内容,对象) 判断是否包含
self.assertEqual(a, b)     # a == b 相等
self.assertNotEqual(a,b)   # a != b
self.assertTrue(表达式)     # 表达式为True
self.assertFalse(表达式)
self.assertIn("文本", 字符串) # 包含文本
self.assertNotIn("xxx",str)

UI 自动化场景:查询结果、弹窗提示文字、状态校验,都用断言。
断言失败 → 用例标记 FAILED;代码报错 → 标记 ERROR。

  1. TestLoader 用例加载器
  • unittest.TestLoader().discover(start_dir, pattern="test*.py")
    • start_dir:用例文件夹路径
    • pattern:匹配文件,如test*.py、用*.py
  • 扫描指定目录、py 文件,自动识别继承TestCase、以test_开头的测试方法,打包成测试套件 TestSuite,交给 TestRunner 去运行。

一句话:负责找用例、收集用例

  • discover () 【最常用,批量跑多个文件】
    • start_dir:起始目录,从哪个文件夹开始搜索用例,可以写相对路径 / 绝对路径
    • pattern:文件匹配规则,支持通配符*
      • pattern="test*.py":匹配 testCase.py、test_login.py
    • top_level_dir:顶层目录,一般不用填,默认 None

返回值:TestSuite 测试套件,里面打包了所有找到的测试用例

  1. 遍历start_dir下,符合pattern的.py文件
  2. 在 py 文件中,找到所有继承unittest.TestCase的类
  3. 在类里面,自动收集以 test_ 开头的方法作为测试用例
  4. 非test_开头的方法(封装的业务函数enter_user_management_page)不会当成用例执行
  • loadTestsFromTestCase () 【加载单个测试类】
    只加载一个测试类里面所有test_开头的用例

  • loadTestsFromModule () 加载整个模块

  • loadTestsFromName () 按名字加载
    可以精准指定:模块。类. 方法,适合执行单条用例

  1. TestRunner 运行器
  • 执行收集好的用例,输出结果
  • 原生:unittest.TextTestRunner() 控制台打印结果
    • 只在控制台打印执行结果,不会生成 html 文件
    • verbosity:日志等级
      • 0:静默
      • 1:简单结果(默认)
      • 2:详细输出,每条用例成功 / 失败都打印(自动化调试推荐)
  • 第三方:unittestreport.TestRunner 生成 HTML 可视化报告
    • 执行用例,自动生成 HTML 测试报告
    • suite:必填,传入 TestSuite 对象(TestLoader 加载出来的用例集合)
    • filename:报告名称,不要后缀;代码内部自动拼接.html
    • report_dir:报告存放文件夹,不存在会自动创建
    • title:HTML 报告页面标题
    • tester:测试人
    • desc:报告描述信息

    runner.run():启动执行所有用例,必须调用 run () 才会开始跑用例 + 生成报告

    from unittestreport import TestRunner
    import os
    import unittest
    
    if __name__ == '__main__':
        test_path = os.getcwd()
        loader = unittest.TestLoader()
        suite = loader.discover(test_path, pattern="test*.py")
    
        runner = TestRunner(
            suite=suite,                # 必填:测试套件
            filename="ui_auto_report",  # 报告文件名,不需要加 .html
            report_dir="./",            # 报告保存目录
            title="后台管理系统UI自动化测试报告",
            tester="鹿",
            desc="后台管理系统自动化测试执行结果"
        )
        runner.run()
    

执行流程

  1. TestRunner 接收 suite
  2. 遍历 suite 里所有测试类、测试用例
  3. 自动执行每个用例对应的 Fixture(setUp → test 方法 → tearDown)
  4. 捕获每条用例结果:成功 / 失败 / 错误
    • success:断言通过
    • failure:断言失败 AssertionError
    • error:代码异常(找不到元素、超时等 Exception)
  5. 汇总统计,写入 HTML 报告 / 控制台输出

执行顺序规则

  1. 先执行 Fixture,再执行 test 用例
  2. 同一个类内,test 用例按照方法名字符串排序执行,不是代码从上往下顺序
    例:test_01 在 test_02 前面执行

运行方式

  1. 直接脚本末尾写 unittest.main(),右键运行
  2. 命令行运行
# 运行整个文件
python testCase.py
# 运行指定类
python -m unittest testCase.DemoTest
# 运行单个用例
python -m unittest testCase.DemoTest.test_case1

常用装饰器

@unittest.skip("暂时跳过该用例")
def test_xxx(self):
    pass

@unittest.skipIf(条件,"条件成立跳过")
def test_xxx(self):
    pass

高频坑点(遇到过的)

  1. ❌ tearDownClass 忘记写@classmethod,报参数缺失错误
  2. ❌ 测试方法不写test_开头,框架识别不到用例
  3. ❌ 用例执行顺序:默认按方法名字母排序,不是代码从上到下顺序
  4. ❌ setUpClass 里面使用 self,必须用 cls
  5. ❌ 混用隐式等待 + 显式等待,容易出现超时异常
  6. ❌ 用中文文件名,discover 加载容易出问题
  7. ❌ 忘记传 suite 参数,直接传 TestLoader 对象,会报错
  8. ❌ filename 写完整 xxx.html,会生成 xxx.html.html 多余后缀
  9. ❌ 多次调用 runner.run(),会重复执行两遍所有用例
  10. ❌ 中文文件名、中文路径,偶尔出现报告生成失败
  11. ❌ 执行完脚本,浏览器闪退、进程残留:Fixture 的 tearDown/tearDownClass 没有写driver.quit()
  12. ❌ 找不到用例:TestLoader 的 pattern 匹配不到文件,Runner 拿到空套件,报告为空
  13. 一定要加断言,没有断言只能叫 “脚本”,不算测试用例

概念区分

  • ERROR:代码异常(报错,比如找不到元素、语法错)
  • FAILED:代码正常执行,断言不满足(业务结果不符合预期)

实例

import os
import unittest
from time import sleep, strftime

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from unittestreport import TestRunner

# 显式等待 指定等待某个【条件】成立,达到条件立刻继续执行;超过设定最大时间还不满足,抛出 TimeoutException。
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 常量抽离,方便统一维护
BASE_URL = "http://49.233.201.254:8080/cms/manage/login.do"

class AutoUiTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls): # 类所有用例执行之前,执行 1 次;必须加装饰器 @classmethod,第一个参数必须是 cls,不能写 self
        cls.opt = Options()
        cls.opt.add_experimental_option('detach', True)
        cls.wait = None

    @classmethod
    def tearDownClass(cls): # 在整个测试类【所有测试用例全部执行完成之后,只执行 1 次】;必须加装饰器 @classmethod,第一个参数必须是 cls,不能写 self
        print('全部用例执行完毕')

    def setUp(self): # 每条用例执行前都会跑
        self.driver = webdriver.Chrome(options=self.opt)
        self.wait = WebDriverWait(self.driver, 10) # # 参数1:driver浏览器对象;参数2:最长等待秒数
        # self.driver.implicitly_wait(4) # 显式等待和隐式等待混用,会带来超时不可预期,直接删除。
        self.driver.get(BASE_URL)
        # 登录
        # 简单静态页面,只需要查找元素,不需要判断可见、可点击的时候。
        # UI 自动化项目,工业上主流做法:废弃隐式等待,全部使用显式等待,稳定性更好。
        self.wait.until(EC.element_to_be_clickable((By.ID,'userAccount'))).send_keys('admin')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'loginPwd'))).send_keys('123456')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'loginBtn'))).click()
    def tearDown(self): # 每条用例执行完都会跑
        sleep(5)
        self.driver.quit()

    # ==========封装公共方法==========
    def enter_user_management_page(self):
        ''' 进入用户管理页面,封装此重复操作 '''
        self.wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="menu-user"]/dt'))).click()
        self.wait.until(EC.element_to_be_clickable((By.XPATH,'//*[@id="menu-user"]/dd/ul/li[1]/a'))).click()
        # 切换iframe
        iframe_in = self.wait.until(EC.presence_of_element_located((By.XPATH,'//*[@id="iframe_box"]/div[2]/iframe')))
        self.driver.switch_to.frame(iframe_in)

    def back_to_main_page(self):
        """退出iframe,切回主页面"""
        self.driver.switch_to.default_content()

    # ==========测试用例==========
    def test_uc_user_management(self):
        ''' 打开 用户中心 - 用户管理页面 '''
        self.enter_user_management_page()
        # 断言:校验搜索按钮存在,证明页面打开成功
        self.assertTrue(self.wait.until(EC.presence_of_element_located((By.ID,'searchBtn'))))
        self.back_to_main_page()

    def test_uc_user_management_search(self):
        ''' 用户管理页面 查询功能 时间范围 '''
        self.enter_user_management_page()
        self.wait.until(EC.element_to_be_clickable((By.ID,'datemin'))).send_keys('2026-09-12')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'datemax'))).send_keys('2026-09-20')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchBtn'))).click()
        # 断言:判断表格加载完成
        self.wait.until(EC.presence_of_element_located((By.ID, 'sysUserTab')))
        self.back_to_main_page()

    def test_uc_user_management_search_phone(self):
        ''' 用户管理页面 查询手机号为13245611111的用户 '''
        self.enter_user_management_page()
        self.wait.until(EC.element_to_be_clickable((By.ID,'searchValue'))).send_keys('13245611111')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchBtn'))).click()
        # 断言:表格包含手机号
        self.assertTrue(self.wait.until(EC.presence_of_element_located(
            (By.XPATH, '//*[@id="sysUserTab"]//tr[contains(., "13245611111")]')
        )))
        self.back_to_main_page()

    def test_uc_user_management_search_email(self):
        ''' 用户管理页面 - 根据邮箱 autotest@163.com 查询用户 '''
        self.enter_user_management_page()
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchValue'))).send_keys('autotest@163.com')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchBtn'))).click()
        # 断言
        self.assertTrue(self.wait.until(EC.presence_of_element_located(
            (By.XPATH, '//*[@id="sysUserTab"]//tr[contains(., "autotest@163.com")]')
        )))
        self.back_to_main_page()

    def test_uc_user_management_search_username(self):
        ''' 用户管理页面 - 根据用户名 zzz 查询用户 '''
        self.enter_user_management_page()
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchValue'))).send_keys('zzz') # 模糊查询
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchBtn'))).click()
        self.back_to_main_page()

    def test_uc_user_management_search_stop(self):
        ''' 用户管理页面 - 停用|启用用户 SBB5886 '''
        self.enter_user_management_page()
        # 查询
        self.wait.until(EC.element_to_be_clickable((By.ID,'searchValue'))).send_keys('SBB5886')
        self.wait.until(EC.element_to_be_clickable((By.ID, 'searchBtn'))).click()
        # 添加步骤 等待表格加载出来,才能点击停用按钮 - 等"包含 SBB5886 的那一行"出现,再把按钮搜索范围限定在该行
        self.wait.until(EC.presence_of_element_located((By.XPATH,'//*[@id="sysUserTab"]//tr[contains(., "SBB5886")]/td')))
        # get_property(一般用来获取DOM原生属性,title两种都可以)
        before_status = self.wait.until(EC.presence_of_element_located((By.XPATH,'//*[@id="sysUserTab"]/tr[1]/td[9]/a[1]')))
        before_title = before_status.get_property("title")

        status_btn = self.wait.until(EC.element_to_be_clickable((By.XPATH,'//*[@id="sysUserTab"]/tr[1]/td[9]/a[1]')))
        status_btn.click()
        self.wait.until(EC.presence_of_element_located((By.CLASS_NAME,'xubox_layer')))
        self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME,'xubox_yes'))).click()

        # 新增第一步:等待弹窗消失!!
        self.wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, 'xubox_layer')))
        # 第二步:等待旧按钮元素失效
        self.wait.until(EC.staleness_of(before_status))

        # 断言 判断前后两个状态不同
        after_status = self.wait.until(
            EC.presence_of_element_located((By.XPATH, '//*[@id="sysUserTab"]//tr[contains(.,"SBB5886")]/td[9]/a[1]')))
        after_title = after_status.get_property("title")
        self.assertTrue(before_title != after_title)

        self.back_to_main_page()


        ''' 
        遇到陈旧元素引用异常的情况 -  找到了元素,拿到了元素对象;但是页面 DOM 刷新 / 表格重载,旧元素已经消失了,再调用 .click() 就报错。
        常见触发场景:查询后表格异步渲染、翻页、筛选,table 的 tr 重新生成。

        `self.wait.until(EC.element_to_be_clickable(...)).click()` 看似是"查找+点击"连在一起,但实际上是 两步 : 
        1. `until(...)` 内部反复查找,直到条件满足, 返回那个元素对象 
        2. `.click()` 在返回的元素对象上调用点击 
        这两步之间只要有 几毫秒 的间隔,如果此时表格正在异步刷新(搜索后表格 tr 重新生成),第 1 步拿到的元素对象就变成了"尸体",第 2 步`.click()` 就报 stale。
        '''
if __name__ == '__main__':
    # unittest.main() # unittest.main() 一旦执行,就会直接运行所有测试用例,运行完成之后,直接退出程序!

    # 直接加载当前类里面的用例,不要用discover(避免文件匹配问题)
    # test_suite = unittest.TestSuite()
    # test_suite.addTests(unittest.TestLoader().loadTestsFromTestCase(AutoUiTest)) # loadTestsFromTestCase(AutoUiTest) 自动获取这个类下面所有以 `test_` 开头的测试方法
    # test_suite.addTest(AutoUiTest('test_uc_user_management_search_stop')) # 测试单个用例
    # unittest.TextTestRunner(verbosity=2).run(test_suite) # 跑测试用例

    test_path = os.getcwd()
    print(test_path)
    test_loader = unittest.TestLoader().discover(start_dir=test_path,pattern='test*.py') # 文件名为中文时,会读取失败,discover找不到脚本
    # unittest.TextTestRunner().run(test_loader)
    test_time = strftime('%y-%m-%d_%H-%M-%S')
    test_report = test_path + '/' + str(test_time) + ' ui_auto.html'
    # test_write = open(test_report, 'wb')
    TestRunner(
        suite = test_loader,
        report_dir = test_path,
        filename = f'{str(test_time)} ui_auto',
        title = '后台管理系统测试报告',
        tester = '鹿',
        desc = '后台管理系统自动化测试报告',
    ).run()

讲解流程

  1. 导入相关模块:
  • unittest模块 import unittest;
  • selenium模块 from selenium import webdriver
  • 时间模块 from time import *
  • os模块 import os
  • unittestreport模块 from unittestreport import TestRunner
  • 等
  1. 创建一个类,继承 unittest.TestCase
  2. 编写fixture 前置后置(打开关闭浏览器)
  3. 编写test_开头的测试用例,封装公共方法,断言
  4. 入口:TestLoader.discover() 搜集全部用例,生成TestSuite
  5. TsetRunner.run() 执行套件,生成HTML报告
  6. 查看报告,分析 error/failure
posted @ 2026-10-09 14:22  一只鹿吖  阅读(8)  评论(0)    收藏  举报