软件测试资源笔记(4万字,持续更新中)
web自动化selenium和unittest篇
selenium部分
基础代码
import time from selenium import webdriver
from selenium.webdriver.common.by import By options = webdriver.EdgeOptions() # 创建浏览器配置对象
options.add_experimental_option("detach", True) # 设置浏览器与脚本分离
driver = webdriver.Chrome(options=options) # 启动浏览器
# 如果使用谷歌浏览器
# service = Service(r"E:\chromedriver-win64\chromedriver.exe")
# driver = webdriver.Chrome(service=service)driver.get(r"C:\Users\86189\PycharmProjects\PythonProject\a.html") driver.find_element(By.XPATH,"//input[@type='text']").send_keys("warren")
driver.find_element(By.XPATH,"//input[@type='password']").send_keys("1111") driver.find_element(By.XPATH,"//button[text()='登录']").click() # 打开百度
time.sleep(4) driver.quit()
driver.find_element()
-
用于查找网页上的一个元素,返回一个
WebElement
对象 -
需要传入两个参数:
-
定位方式(
By
提供的枚举常量) -
定位值(对应的选择器)
-
By
定位方式
By
是 Selenium 提供的一个类,封装了各种元素定位方式。常用有:
-
By.ID("id值")
→ 通过元素的 id 属性定位 -
By.NAME("name值")
→ 通过 name 属性定位 -
By.CLASS_NAME("class值")
→ 通过 class 属性定位 -
By.TAG_NAME("tag值")
→ 通过标签名定位(如"input"
,"button"
) -
By.LINK_TEXT("文本")
→ 通过完整文本定位, 比如这种
-
By.PARTIAL_LINK_TEXT("部分文本")
→ 通过链接的部分文本定位 -
By.CSS_SELECTOR("css选择器")
→ 通过 CSS 选择器定位 -
By.XPATH("xpath表达式")
→ 通过 XPath 表达式定位(最万能)
.send_keys()
-
作用:模拟键盘输入
-
参数:字符串或者特殊键(如回车键
Keys.ENTER
)
element.send_keys("hello") # 输入字符串
element.send_keys(Keys.ENTER) # 模拟回车
element.send_keys("123", Keys.TAB) # 输入 123 后按下 TAB
XPath定位方式
例如:
<div id="login"><input type="text" name="username" /><input type="password" name="password" /><button>登录</button>
</div>
XPath 的基本语法:
表达式 | 含义 |
---|---|
/ | 从根节点开始定位(绝对路径) |
// | 从任意层级查找(相对路径,最常用) |
. | 当前节点 |
.. | 上一级节点 |
@ | 属性(attribute) |
* | 通配符,匹配任意节点或属性 |
常见 XPath 定位方式:
定位方式 | 示例 | 含义 |
---|---|---|
1️⃣ 通过标签名定位 | //input | 定位页面上所有 <input> 元素 |
2️⃣ 通过属性定位 | //input[@id='username'] | 定位 id 为 “username” 的 input 元素 |
3️⃣ 多属性组合定位 | //input[@type='text' and @name='username'] | 同时匹配多个属性 |
4️⃣ 模糊匹配属性 | //input[contains(@name,'user')] | name 属性中包含 “user” |
5️⃣ 以某字符串开头 | //div[starts-with(@class,'login')] | class 以 “login” 开头的 div |
6️⃣ 精确匹配文本内容 | //button[text()='登录'] | 按钮文字为“登录”的元素 |
7️⃣ 模糊匹配文本内容 | //button[contains(text(),'登')] | 文本中包含“登”的按钮 |
8️⃣ 层级定位(父子关系) | //div[@id='login']/input[@name='username'] | 找到 login 下的 username 输入框 |
9️⃣ 索引定位(同级多个相同标签) | (//input[@type='text'])[1] | 定位第一个文本框 |
🔟 使用通配符匹配任意标签 | //*[@id='login'] | id 为 “login” 的任意元素 |
绝对路径 vs 相对路径:
类型 | 示例 | 特点 |
---|---|---|
绝对路径 | /html/body/div[1]/input | 从根节点开始,容易受页面结构变化影响 |
相对路径 | //input[@id='username'] | 从任意位置查找,更常用、更稳定 |
XPath 中的常用函数:
函数 | 用法 | 示例 |
---|---|---|
contains() | 包含子串 | //div[contains(@class,'main')] |
starts-with() | 属性以某字符串开头 | //input[starts-with(@name,'user')] |
text() | 匹配文本内容 | //a[text()='首页'] |
last() | 最后一个节点 | (//li)[last()] |
position() | 指定位置 | (//input)[position()=2] |
normalize-space() | 去掉空格匹配文本 | //span[normalize-space(text())='确定'] |
相对路径转绝对路径的方法
import os file_path = os.path.abspath("openChrome.py")
url = f"file:///{file_path.replace(os.sep, '/')}" print(url)
CSS Selector 基本语法
假设 HTML:
<div id="loginBox"><input id="userA" type="text" class="input-text" placeholder="用户名"><input id="passwordA" type="password" class="input-text" placeholder="密码"><button class="btn login-btn">登录</button>
</div>
(1) 按 id 定位
driver.find_element(By.CSS_SELECTOR, "#userA").send_keys("admin")
#userA
→ 选择 id 为userA
的元素
(2) 按 class 定位
driver.find_element(By.CSS_SELECTOR, ".login-btn").click()
.login-btn
→ 选择 class 包含login-btn
的元素
(3) 按标签+class
driver.find_element(By.CSS_SELECTOR, "button.login-btn").click()
button.login-btn
→ 限制标签为<button>
且 class 包含login-btn
(4) 按属性定位
driver.find_element(By.CSS_SELECTOR, "input[placeholder='用户名']").send_keys("admin")
driver.find_element(By.CSS_SELECTOR, "input[type='password']").send_keys("123456")
-
[属性名='值']
→ 匹配属性值 -
也可以模糊匹配:
input[placeholder*='用'] # 属性值包含“用”
input[type^='pass'] # 属性值以“pass”开头
input[type$='word'] # 属性值以“word”结尾
浏览器操作方法
方法 | 功能描述 |
---|---|
maximize_window() | 最大化浏览器窗口 |
set_window_size(width, height) | 设置窗口宽高(像素) |
set_window_position(x, y) | 设置窗口在屏幕的坐标位置 |
back() | 浏览器后退(模拟点击后退按钮) |
forward() | 浏览器前进(模拟点击前进按钮) |
refresh() | 刷新页面(模拟 F5 按键) |
close() | 关闭当前窗口 |
quit() | 关闭浏览器驱动(结束所有窗口、释放资源) |
title | 获取页面标题(属性,调用无括号,如 driver.title ) |
current_url | 获取当前页面 URL(属性,调用无括号,如 driver.current_url ) |
元素信息获取方法
方法 | 功能描述 |
---|---|
size | 获取元素尺寸(宽高,属性,调用无括号,如 element.size ) |
text | 获取元素文本内容(属性,调用无括号,如 element.text ) |
get_attribute("xxx") | 获取元素指定属性值(xxx 为属性名,如 get_attribute("id") ) |
is_displayed() | 判断元素是否可见(返回布尔值) |
is_enabled() | 判断元素是否可用(如按钮是否可点击,返回布尔值) |
is_selected() | 判断元素是否被选中(复选框、单选框专用,返回布尔值 ) |
frame/iframe 标签操作
-
iframe
是一个嵌套的 HTML 文档 -
Selenium 默认操作的是 最外层 HTML
-
如果元素在 iframe 里,直接用
find_element
/find_elements
是找不到的
#进入框架
# 方式1:通过 id 或 name
driver.switch_to.frame("frame_id_or_name")# 方式2:通过 WebElement 对象
iframe_element = driver.find_element(By.TAG_NAME, "iframe")
driver.switch_to.frame(iframe_element)# 出框架
driver.switch_to.default_content()
下拉框操作
Select 类用于操作 select
标签
ele = driver.find_element(By.XPATH,"//select[@name='cat_id']")
sel = Select(ele)sel.select_by_index(2)sel.select_by_value("2")sel.select_by_visible_text("手机类型")
#实例化对象:
select = Select (element)
element: <select>标签对应的元素,通过元素定位方式获取,
例如:driver.find_element_by_id ("selectA")
方法:
- select_by_index (index) --> 根据 option 索引来定位,从 0 开始
- select_by_value (value) --> 根据 option 属性 value 值来定位
- select_by_visible_text (text) --> 根据 option 显示文本来定位
Cookie 操作
方法 | 说明 | 示例 |
---|---|---|
driver.get_cookies() | 获取当前页面所有 Cookie,返回列表 | cookies = driver.get_cookies() |
driver.get_cookie(name) | 获取指定名字的 Cookie | cookie = driver.get_cookie("PHPSESSID") |
driver.add_cookie(cookie_dict) | 添加 Cookie 到浏览器 | driver.add_cookie({"name":"PHPSESSID","value":"xxxx"}) |
driver.delete_cookie(name) | 删除指定 Cookie | driver.delete_cookie("PHPSESSID") |
driver.delete_all_cookies() | 删除所有 Cookie | driver.delete_all_cookies() |
link_text
和 partial_link_text
的区别
方法 | 含义 | 匹配方式 |
---|---|---|
link_text | 按超链接的完整文本定位 | 完全匹配,文本必须完全一样 |
partial_link_text | 按超链接文本的部分定位 | 模糊匹配,只要包含指定子串即可 |
都只能用于 <a>
标签(超链接)
常用鼠标操作
使用 ActionChains
后必须调用 .perform()
执行
# 导包
from selenium.webdriver import ActionChains
from selenium import webdriver
from selenium.webdriver.common.by import Bydriver = webdriver.Chrome()
driver.get("https://www.baidu.com")element = driver.find_element(By.ID, "kw") # 搜索框
#注册
actions = ActionChains(driver)
方法 | 作用 | 示例 |
---|---|---|
click() | 单击元素 | actions.click(element).perform() |
double_click() | 双击元素 | actions.double_click(element).perform() |
context_click() | 右击元素 | actions.context_click(element).perform() |
move_to_element() | 鼠标悬停到元素 | actions.move_to_element(element).perform() |
click_and_hold() | 鼠标按下不放 | actions.click_and_hold(element).perform() |
release() | 鼠标释放 | actions.release(element).perform() |
drag_and_drop(source, target) | 拖拽元素 | actions.drag_and_drop(source, target).perform() |
drag_and_drop_by_offset(source, x, y) | 按偏移拖拽 | actions.drag_and_drop_by_offset(source, 100, 0).perform() |
键盘操作
from selenium.webdriver.common.keys import Keyselement = driver.find_element(By.ID, "kw")# 输入文字 + 回车
element.send_keys("Selenium", Keys.ENTER)# 组合操作
actions = ActionChains(driver)
actions.key_down(Keys.SHIFT).send_keys("selenium").key_up(Keys.SHIFT).perform() # 大写输入
键 | 描述 |
---|---|
Keys.ENTER | 回车 |
Keys.TAB | Tab 键 |
Keys.ESCAPE | Esc |
Keys.BACKSPACE | 删除 |
Keys.CONTROL | Ctrl |
Keys.ALT | Alt |
Keys.SHIFT | Shift |
Keys.ARROW_UP/DOWN/LEFT/RIGHT | 上下左右方向键 |
Keys.DELETE | 删除键 |
Keys.COPY | Ctrl+C(复制) |
Keys.PASTE | Ctrl+V(粘贴) |
元素等待
隐式等待(全局等待):
from selenium import webdriverdriver = webdriver.Chrome()
driver.implicitly_wait(10) # 设置全局等待时间 10 秒driver.get("https://www.baidu.com")
driver.find_element("id", "kw").send_keys("ChatGPT")
显式等待:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ECdriver = webdriver.Chrome()
driver.get("https://www.baidu.com")# 显式等待:最多等待 10 秒,直到元素出现
search_box = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "kw"))
)
search_box.send_keys("ChatGPT")
显式与隐式区别:
-
作用域:隐式为全局元素,显式等待为单个元素有效
-
使用方法:隐式等待直接通过驱动对象调用,而显式等待方法封装在
WebDriverWait
类中 -
达到最大超时时长后抛出的异常不同:隐式为
NoSuchElementException
,显式等待为TimeoutException
unitTest部分
TestCase
、TestSuite
、TestLoader
、TestRunner
—— 是 Python unittest
框架的四大核心组成部分
可以这样理解:
组件 | 作用 |
---|---|
TestCase | 最小的测试单元(一个测试用例) |
TestSuite | 测试集合(可包含多个 TestCase 或 TestSuite) |
TestLoader | 负责从模块或类中批量收集用例,组成 TestSuite |
TestRunner | 负责执行 Suite 并输出结果(控制台、报告等) |
TestCase
-
每一个测试类都继承自
unittest.TestCase
-
每一个以
test_
开头的方法都是一个独立测试用例
from selenium import webdriver
import time
import unittest from selenium.webdriver.common.by import By class TestUnit1(unittest.TestCase): # 初始化环境 def setUp(self): # 1、self 就是类的引用/实例 # 2、全局变量的定义:self.变量名 self.driver = webdriver.Edge() self.driver.maximize_window() self.url = "https://www.baidu.com/" self.driver.get(self.url) time.sleep(3) # 在百度中搜索信息 # 测试用例的命名: test_ def test_search1(self): self.driver.find_element(By.ID, "chat-textarea").send_keys("java") self.driver.find_element(By.ID,"chat-submit-button").click() time.sleep(6) def test_search2(self): self.driver.find_element(By.ID, "chat-textarea").send_keys("python") self.driver.find_element(By.ID,"chat-submit-button").click() time.sleep(6) # 关闭浏览器 def tearDowm(self): self.driver.quit() # 一个入口 if __name__ == "__main__": unittest.main()
TestSuite —— 测试集合
-
用来组织多个测试用例或测试类
-
也可以嵌套多个
TestSuite
形成分层结构
示例:
-
实例化:
suite = unittest.TestSuite ()
-
添加用例:
suite.addTest (ClassName ("MethodName"))
(ClassName:为类名;MethodName:为方法名) -
添加扩展:
suite.addTest (unittest.makeSuite (ClassName))
(一次性把某个类里所有以test_
开头的方法都加进来)
import unittest class TestMath(unittest.TestCase): def test_add(self): print("运行 test_add") self.assertEqual(1 + 1, 2) def test_sub(self): print("运行 test_sub") self.assertEqual(5 - 3, 2)import unittest from demo import TestMath if __name__ == "__main__": # 创建一个 TestSuite suite = unittest.TestSuite() # 添加单个测试方法 suite.addTest(TestMath("test_add")) # 添加多个测试方法 suite.addTests([TestMath("test_sub")]) # 创建一个 runner(运行器)来执行 suite runner = unittest.TextTestRunner() runner.run(suite)
TestRunner —— 测试运行器
-
负责执行测试用例,并输出结果
-
默认运行器:
unittest.TextTestRunner()
(打印在终端) -
也可以用第三方运行器生成报告,比如
HTMLTestRunner
示例:
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
输出:
test_add (__main__.TestMath) ... ok
test_sub (__main__.TestMath) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000sOK
TestLoader
从 测试类、测试模块(文件)、 目录中加载 test_
开头的用例,然后放到 TestSuite
里
就像一个“收集器”, 把符合条件的测试方法收集起来,交给 TestSuite
组织,最后由 TextTestRunner
执行
方法 | 作用 |
---|---|
loadTestsFromTestCase(TestCaseClass) | 从一个测试类里加载所有 test_ 开头的方法 |
loadTestsFromModule(module) | 从一个模块(文件)里加载所有测试用例 |
loadTestsFromName(name, module=None) | 按名字加载某个测试用例或类 |
loadTestsFromNames(names, module=None) | 按名字批量加载多个测试用例或类 |
discover(start_dir, pattern="test*.py") | 在目录下自动搜索所有符合命名规则的测试文件 |
import unittestclass TestMath(unittest.TestCase):def test_add(self):print("运行 test_add")self.assertEqual(1+1, 2)def test_sub(self):print("运行 test_sub")self.assertEqual(5-3, 2)if __name__ == "__main__":loader = unittest.TestLoader()suite = loader.loadTestsFromTestCase(TestMath) # 加载 TestMath 里的所有 test_ 方法runner = unittest.TextTestRunner(verbosity=2)runner.run(suite)
组合使用:
import unittestclass TestMath(unittest.TestCase):def test_add(self):self.assertEqual(1 + 2, 3)def test_sub(self):self.assertEqual(5 - 2, 3)class TestString(unittest.TestCase):def test_upper(self):self.assertEqual("hello".upper(), "HELLO")def test_isupper(self):self.assertTrue("HELLO".isupper())def suite():suite = unittest.TestSuite()loader = unittest.TestLoader()suite.addTests(loader.loadTestsFromTestCase(TestMath))suite.addTests(loader.loadTestsFromTestCase(TestString))return suiteif __name__ == "__main__":runner = unittest.TextTestRunner(verbosity=2)runner.run(suite())
断言
序号 | 断言方法 | 断言描述 |
---|---|---|
1 | assertTrue(expr, msg=None) | 验证expr 是true ,如果为false ,则fail |
2 | assertFalse(expr, msg=None) | 验证expr 是false ,如果为true ,则fail |
3 | assertEqual(expected, actual, msg=None) | 验证expected==actual ,不等则fail 【掌握】 |
4 | assertNotEqual(first, second, msg=None) | 验证first != second ,相等则fail |
5 | assertIsNone(obj, msg=None) | 验证obj 是None ,不是则fail |
6 | assertIsNotNone(obj, msg=None) | 验证obj 不是None ,是则fail |
7 | assertIn(member, container, msg=None) | 验证container 里是否包含member |
8 | assertNotIn(member, container, msg=None) | 验证是否member not in container |
参数化
动态传入多组测试数据, 让一个测试方法执行多次, 可以提高测试用例的复用性
安装依赖:
pip install parameterized
@parameterized.expand()
: 为测试方法传入多组参数
import unittest
from parameterized import parameterizeddef add(a, b):return a + bclass TestAdd(unittest.TestCase):# 多组测试数据:(输入a, 输入b, 预期结果)test_data = [(1, 2, 3), (0, 0, 0), (-1, 1, 0), (2.5, 3.5, 6) ]# 用parameterized.expand装饰测试方法,传入测试数据@parameterized.expand(test_data)def test_add(self, a, b, expected):result = add(a, b)self.assertEqual(result, expected) # 断言结果是否符合预期if __name__ == "__main__":unittest.main()
跳过
用于跳过执行测试函数和测试类
@unittest.skip(reason)
:无条件跳过
import unittestclass TestExample(unittest.TestCase):@unittest.skip("该功能已废弃,无需测试") # 无条件跳过def test_old_feature(self):self.assertEqual(1 + 1, 2) # 此用例不会执行
@unittest.skipIf(condition, reason)
:条件满足时跳过
import unittest
import sysclass TestExample(unittest.TestCase):@unittest.skipIf(sys.platform == "win32", "Windows系统不支持此功能")def test_linux_feature(self):# 仅在非Windows系统执行self.assertTrue("linux" in sys.platform.lower())
PO 模式:
一种设计模式,用于优化 UI 自动化测试代码的结构,提高代码的可维护性、复用性和可读性
组成:
-
页面类
- 每个页面(或功能模块)对应一个类,例如登录页(LoginPage)、首页(HomePage)
- 类中包含页面的所有元素定位(如按钮、输入框、链接等)
- 类中封装该页面的所有操作方法(如输入用户名、点击登录、获取提示信息等)
-
测试用例
- 不直接操作页面元素,而是调用
页面类
的方法来完成测试步骤 - 专注于测试逻辑(如数据准备、步骤组合、断言验证等)
- 不直接操作页面元素,而是调用
-
基础层
- 封装通用的操作方法(如元素查找、点击、输入、等待等),所有页面类继承该基础类,避免代码重复
示例:
- 基础层(BasePage)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ECclass BasePage:def __init__(self, driver):self.driver = driver# 查找元素(显式等待)def find_element(self, locator, timeout=10):return WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(locator))# 点击元素def click_element(self, locator):self.find_element(locator).click()# 输入文本def input_text(self, locator, text):self.find_element(locator).send_keys(text)
- 页面类(LoginPage)
from tkinter.tix import Select from selenium.webdriver.common.by import By from web.BasePage import BasePage class loginPage(BasePage): USERNAME_LOCATE=(By.XPATH,'//*[@id="username"]') PASSWORD_LOCATE=(By.XPATH,'//*[@id="password"]') SEX_LOCATE=(By.XPATH,'//*[@id="gender"]') LIKES_LOCATE=(By.XPATH,'//*[@id="registerForm"]/div/label[1]/input') REGISTER_LOCATE=(By.XPATH,'//*[@id="registerBtn"]') """ 输入用户名 """ def enter_username(self,username): usernameTextArea=self.input_text(self.USERNAME_LOCATE,username) def enter_password(self,password): passwordTextArea=self.input_text(self.PASSWORD_LOCATE,password) def select_sex(self): sex_element = self.find_element(self.SEX_LOCATE) sel = Select(sex_element) sel.select_by_index(2) def choose_likes(self): likes=self.click_element(self.LIKES_LOCATE) def click_register(self): r=self.click_element(self.REGISTER_LOCATE)
- 测试用例(TestLogin)
import os
import sys
import unittest from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.wait import WebDriverWait sys.path.append(os.path.dirname(__file__))
from selenium.webdriver.support import expected_conditions as EC
from loginPage import * class TestLogin(unittest.TestCase): def setUp(self): # 初始化浏览器 service = Service(r"E:\chromedriver-win64\chromedriver.exe") self.driver = webdriver.Chrome(service=service) # 将相对路径转为绝对路径 file_path = os.path.abspath("test.html") url = f"file:///{file_path.replace(os.sep, '/')}" self.driver.get(url) self.driver.maximize_window() self.login_page = loginPage(self.driver) def test_login_failure(self): self.login_page.enter_username("wrong_user") self.login_page.enter_password("wrong_pwd") self.login_page.choose_likes() self.login_page.click_register() popup = WebDriverWait(self.driver, 5).until( EC.visibility_of_element_located((By.ID, "popup")) ) self.assertIn("wrong_user", popup.text) # 截图 self.driver.save_screenshot("full_page.png") popup.screenshot("popup.png") print("注册弹窗内容:", popup.text) def tearDown(self): self.driver.quit() if __name__ == "__main__": unittest.main()
两个方法:
setUp(self)
:
🔹 作用:在每个测试用例运行之前执行,用于做“初始化”操作
tearDown(self)
:
🔹 作用:在每个测试用例执行后自动执行,用于做“清理”工作。
如果有多个测试方法,比如:
def test_login_success(self):...def test_login_failure(self):...
执行顺序是:
步骤 | 执行内容 |
---|---|
1 | setUp() |
2 | test_login_success() |
3 | tearDown() |
4 | setUp() |
5 | test_login_failure() |
6 | tearDown() |
unittest
框架中的命名规则:
- 测试类名必须以
Test
开头, 并且必须继承自unittest.TestCase
例如:
class TestLogin(unittest.TestCase): # ✅ 正确...
- 测试方法必须以
test_
开头
例如:
def test_login_success(self): # ✅ 正确...def test_login_failure(self): # ✅ 正确...
类型 | 识别规则 |
---|---|
测试文件 | 文件名以 test 开头,例如 test_login.py |
测试类 | 类名以 Test 开头 |
测试方法 | 方法名以 test_ 开头 |
一些细节
元素操作的返回值与链式调用问题
element.click().send_keys("1111")
问题在于:
-
element.click()
的返回值是 None; -
所以
.send_keys("1111")
等于在None
上调用,必然报错。
正确写法应该分两步:
element.click()
element.send_keys("1111")
总结:
操作 | 返回值 | 是否可链式调用 |
---|---|---|
click() | None | ❌ 不可链式 |
send_keys() | None | ❌ 不可链式 |
find_element() | WebElement 对象 | ✅ 可链式 |
WebDriverWait(...).until(...) | WebElement 对象 | ✅ 可链式 |
🧩 正确可链式写法示例:
driver.find_element(By.ID, "username").send_keys("abc")
元素清空输入问题
如果一个输入框已有值:
element.send_keys("新值")
会直接追加在旧值后面。
正确写法:
element.clear()
element.send_keys("新值")
Pytest测试框架篇
Python篇
type
函数
a = 100
b = 123.45
c = 'hello, world'
d = True
print(type(a)) # <class 'int'>
print(type(b)) # <class 'float'>
print(type(c)) # <class 'str'>
print(type(d)) # <class 'bool'>
输入 input()
变量 = input("提示信息:")
函数返回值类型是字符串
身份运算符:is
、is not
作用:
判断 两个变量是否引用同一个对象(内存地址是否相同)。
不是判断“值是否相等”,而是判断“是不是同一个对象”。
示例:
a = [1, 2, 3]
b = a
c = [1, 2, 3]print(a is b) # True → b 指向同一个对象
print(a is c) # False → 虽然值相等,但不是同一个对象
print(a == c) # True → 值相等
==
判断的是值是否相等
成员运算符:in
、not in
作用:
判断 某个元素是否存在于序列(字符串、列表、元组、字典、集合)中。
示例:
nums = [1, 2, 3, 4]
print(2 in nums) # True
print(5 not in nums) # Truetext = "hello"
print("h" in text) # True
print("z" in text) # Falsedic = {"name": "Tom", "age": 18}
print("name" in dic) # True (判断 key 是否存在)
print("Tom" in dic) # False (不判断 value)
注意
- 小整数缓存机制
Python 会缓存-5 ~ 256
的整数,所以:
a = 100b = 100print(a is b) # True → 实际上是同一个缓存对象
但:
a = 1000b = 1000print(a is b) # False → 超出缓存范围,不同对象
f-string
格式化
语法:
在字符串前加上 f
,直接在 {}
中写变量或表达式。
name = "小明"
age = 18
print(f"我叫 {name},今年 {age} 岁。")
输出:
我叫 小明,今年 18 岁。
match-case语句
基本语法:
match 变量:case 值1:执行语句1case 值2:执行语句2case _:默认执行语句(相当于 else)
✅ 关键点:
-
match
用来匹配一个表达式或变量; -
case
用来定义匹配的条件; -
_
表示通配符,即“其他情况”; -
匹配成功后,程序就不会继续往下匹配,不需要写
break
简单数字匹配:
command = input("请输入命令编号(1-3):")match command:case "1":print("启动系统")case "2":print("关闭系统")case "3":print("重启系统")case _:print("未知命令")
匹配多个值:
fruit = "apple"match fruit:case "apple" | "pear" | "peach":print("这是一种水果")case "carrot" | "potato":print("这是一种蔬菜")case _:print("未知食物")
进阶技巧:守卫(guard)条件:
你还可以在 case
后加上条件判断:
x = int(input("输入数字:"))match x:case n if n < 0:print("负数")case n if n == 0:print("零")case n if n > 0:print("正数")
列表(list
)总结
列表简介
- 特点:有序, 支持索引访问, 元素可以重复;支持动态扩展
创建列表
items1 = [35, 12, 99] # 使用 [] 创建
items2 = ['Python', 'Java']
items3 = [100, 12.3, 'Python', True]
items4 = list(range(1, 10)) # 使用 list() 构造器
items5 = list('hello')
列表运算
# 拼接
[1,2,3] + [4,5] # [1,2,3,4,5]
# 重复
[1,2] * 3 # [1,2,1,2,1,2]
# 成员判断
3 in [1,2,3] # True
4 not in [1,2,3] # True
索引与切片
- 索引访问:
fruits = ['apple','banana','peach']
fruits[0] # apple
fruits[-1] # peach
fruits[1] = 'orange'
- 切片访问:
fruits[0:2] # ['apple','orange']
fruits[:2] # ['apple','orange']
fruits[::2] # ['apple','peach']
fruits[-2::-1] # ['orange','apple']
- 切片修改:
fruits[0:2] = ['x','y'] # 修改指定区间的元素
添加元素
lst = [1,2,3]
lst.append(4) # [1,2,3,4]
lst.insert(1, 9) # [1,9,2,3,4]
删除元素
lst.remove(9) # 删除指定值,若不存在会报错
lst.pop() # 删除最后一个元素
lst.pop(1) # 删除指定索引元素
del lst[0] # 删除指定索引元素
lst.clear() # 清空列表
查询元素
lst.index(2) # 返回第一个匹配索引
lst.count(2) # 统计元素出现次数
排序与反转
lst.sort() # 升序排序
lst.reverse() # 元素顺序反转
列表遍历
- 通过索引遍历:
for i in range(len(lst)):print(lst[i])
- 直接遍历元素:
for item in lst:print(item)
切片
列表切片的语法为:
list[start:end:stride]
-
start:起始索引,表示从哪个位置开始取元素(包含该位置的元素)。
-
end:结束索引,表示取到哪个位置结束(不包含该位置的元素)。
-
stride:步长,表示索引每次增加的值(可以为负数表示反向取)。
注意:切片不会改变原列表,它返回的是一个新的列表。
正向切片
fruits = ['apple', 'banana', 'cherry', 'date', 'fig', 'grape']# 从索引1取到索引3(不包含3)
print(fruits[1:3]) # ['banana', 'cherry']# 从索引0取到索引5,每次间隔2
print(fruits[0:5:2]) # ['apple', 'cherry', 'fig']# 从索引2取到最后
print(fruits[2:]) # ['cherry', 'date', 'fig', 'grape']# 从开始取到索引3(不包含3)
print(fruits[:3]) # ['apple', 'banana', 'cherry']# 取整个列表
print(fruits[:]) # ['apple', 'banana', 'cherry', 'date', 'fig', 'grape']
切片赋值(修改元素)
fruits = ['apple', 'banana', 'cherry', 'date', 'fig']# 修改索引1到3的元素
fruits[1:4] = ['blueberry', 'cranberry', 'dragonfruit']
print(fruits) # ['apple', 'blueberry', 'cranberry', 'dragonfruit', 'fig']# 删除部分元素
fruits[1:3] = []
print(fruits) # ['apple', 'dragonfruit', 'fig']# 插入元素
fruits[1:1] = ['banana', 'cherry']
print(fruits) # ['apple', 'banana', 'cherry', 'dragonfruit', 'fig']
字符串
s = 'hello'
print(s + ' world') # 拼接
print(s * 3) # 重复
print('h' in s) # True
print('a' > 'A') # True
索引与切片
-
索引:
s[i]
正向从 0 开始,负向从 -1 开始 -
切片:
s[start:end:step]
,返回新字符串,原字符串不变
s = 'abc123'
print(s[0], s[-1]) # a 3
print(s[1:4]) # bc1
print(s[::2]) # ac2
print(s[::-1]) # 321cba
- 注意:字符串不可变,不能通过索引直接修改字符。
遍历
for i in range(len(s)):print(s[i])for ch in s:print(ch)
常用字符串方法
功能 | 方法示例 | 说明 |
---|---|---|
大小写转换 | s.upper() / s.lower() / s.title() / s.capitalize() | 返回新字符串 |
查找 | s.find(sub), s.rfind(sub), s.index(sub) | 返回索引或 -1 / 异常 |
开头/结尾判断 | s.startswith('a'), s.endswith('b') | 返回 True/False |
特性判断 | s.isdigit(), s.isalpha(), s.isalnum() | 检查数字、字母、字母数字 |
修剪 | s.strip(), s.lstrip(), s.rstrip() | 去掉首尾指定字符 |
替换 | s.replace(old, new, count=-1) | 替换字符串 |
拆分/合并 | s.split(sep=None, maxsplit=-1), sep.join(list) | 拆分成列表或合并列表 |
格式化 | '{0} {1}'.format(a,b), f'{a} {b}' | 格式化字符串 |
对齐 | s.center(width, fill), s.ljust(width, fill), s.rjust(width, fill), s.zfill(width) | 居中、左/右对齐、补零 |
编码/解码 | s.encode(encoding), b.decode(encoding) | str ↔ bytes |
集合
特点: 无序,元素不能重复, 不支持索引运算
注意:空集合必须使用 set()
,{}
是空字典。
元素要求:必须是可哈希类型(int
、float
、str
、tuple
等),不可变类型;集合、列表等可变类型不能作为元素。
set1 = {1, 2, 3, 3, 3, 2}
print(set1)set2 = {'banana', 'pitaya', 'apple', 'apple', 'banana', 'grape'}
print(set2)set3 = set('hello')
print(set3)set4 = set([1, 2, 2, 3, 3, 3, 2, 1])
print(set4)set5 = {num for num in range(1, 20) if num % 3 == 0 or num % 7 == 0}
print(set5)
set1 = {1, 2, 3, 4, 5, 6, 7}
set2 = {2, 4, 6, 8, 10}# 交集
print(set1 & set2) # {2, 4, 6}
print(set1.intersection(set2)) # {2, 4, 6}# 并集
print(set1 | set2) # {1, 2, 3, 4, 5, 6, 7, 8, 10}
print(set1.union(set2)) # {1, 2, 3, 4, 5, 6, 7, 8, 10}# 差集
print(set1 - set2) # {1, 3, 5, 7}
print(set1.difference(set2)) # {1, 3, 5, 7}# 对称差
print(set1 ^ set2) # {1, 3, 5, 7, 8, 10}
print(set1.symmetric_difference(set2)) # {1, 3, 5, 7, 8, 10}
集合的方法:
set1 = {1, 10, 100}# 添加元素
set1.add(1000)
set1.add(10000)
print(set1) # {1, 100, 1000, 10, 10000}# 删除元素
set1.discard(10)
if 100 in set1:set1.remove(100)
print(set1) # {1, 1000, 10000}# 清空元素
set1.clear()
print(set1) # set()
元组
-
有序,可以存放多个元素。
-
元组是不可变类型,一旦创建,元素不可修改、删除或增加
-
可以包含任意类型的元素(整数、字符串、布尔值等)
t1 = (35, 12, 98) # 三元组 t2 = ('骆昊', 45, True, '四川成都') # 四元组t3 = 10, 20, 30 # 等价于 (10, 20, 30)print(type(t1)) # 输出:<class 'tuple'>
-
单元素元组需加逗号:
t = (5,)
-
不加逗号的括号不是元组,而是普通类型
-
-
基本操作
-
类型与长度
type(t1) # <class 'tuple'> len(t1) # 元素数量
-
索引运算
t1[0] # 第一个元素 t2[-1] # 最后一个元素
-
切片运算
t2[:2] # 从开头到索引2(不含) t2[::3] # 步长为3
-
-
遍历与成员运算
for elem in t1:print(elem)12 in t1 # True 'Hao' not in t2 # True
-
拼接与比较
-
拼接:
t3 = t1 + t2
,生成新的元组 -
比较:按元素逐一比较大小和相等性
t1 == t3 # False t1 >= t3 # False t1 <= (35, 11, 99) # False
-
字典
-
字典是一种无序的可变容器,以**键值对(key-value)**的形式保存数据。
-
通过键可以快速访问对应的值,非常适合存储关联数据(例如人的信息、商品信息等)。
-
键必须是不可变类型(如
int
、float
、str
、tuple
),值可以是任意类型(可变或不可变)。# 字面量语法 person = {'name': '王大锤', 'age': 55, 'height': 168}# 使用 dict 构造器 person = dict(name='王大锤', age=55, height=168)# 使用 zip 创建字典 items = dict(zip('ABCDE', range(1,6)))# 字典生成式 cubes = {x: x**3 for x in range(1,6)}
-
索引运算:通过键访问或修改值
person['age'] = 25person['tel'] = '13122334455'
- 成员运算:判断键是否存在
'name' in person # True'tel' in person # False
- 循环遍历:遍历字典的键,或用
items()
遍历键值对
for key, value in person.items():print(f'{key}: {value}')
字典方法:
- 获取值:
get
方法避免KeyError
person.get('age') # 25person.get('sex', '男') # '男'
- 获取键、值、键值对:
person.keys()person.values()person.items()
- 更新字典:
update
或|=
person.update({'age': 30, 'addr': '成都'})person |= {'age': 30, 'addr': '成都'}
- 删除元素:
pop
、popitem
、del
person.pop('age') # 返回被删除的值person.popitem() # 返回被删除的键值对del person['addr'] # 删除指定键person.clear() # 清空字典
空函数
空函数指的是没有实现功能,只是一个占位的函数。
有时候我们在写程序时,函数的具体实现还没想好,但又想先写好结构,这时就需要空函数。
例如:
def my_function():pass
pass
的作用
Python 不允许定义空代码块。
比如:
def func():# 什么都不写
这会报错:
IndentationError: expected an indented block
pass
就是用来防止语法错误的占位符
pass
是一个空语句,执行时什么也不做
# 示例1:空函数中使用 pass
def func():pass# 示例2:空类中使用 pass
class Student:pass# 示例3:空循环中使用 pass
for i in range(5):pass# 示例4:空条件中使用 pass
if True:pass
函数参数
默认参数
- 如果传了参数 → 使用传入的值
- 如果没传 → 使用默认值
def greet(name="游客"):print("你好,", name)greet() # 输出:你好, 游客
greet("小明") # 输出:你好, 小明
注意默认参数要放在普通参数之后:
def func(a, b=10): # ✅ 正确passdef func(b=10, a): # ❌ 错误pass
可变参数(*args
)
*args
可以接收 任意数量的位置参数,在函数内部是一个元组。
def show_numbers(*args):print(args)show_numbers(1, 2, 3)
# 输出:(1, 2, 3)
关键字参数(**kwargs
)
**kwargs
可以接收 任意数量的“键=值”形式参数,是一个字典
def show_info(**kwargs):print(kwargs)show_info(name="小明", age=18)
# 输出:{'name': '小明', 'age': 18}
面向对象
构造方法 __init__
构造方法名字固定为 __init__
:
class Student:def __init__(self, name, score):self.name = nameself.score = scorebart = Student("Bart", 59)
print(bart.name, bart.score) # Bart 59
实例方法
- 作用:普通的方法,需要通过对象调用,操作对象的属性, 第一个参数必须是
self
,表示当前对象本身。
class Student:def __init__(self, name, score):self.name = nameself.score = scoredef print_score(self):print(f"{self.name}: {self.score}")bart = Student("Bart", 59)
bart.print_score() # Bart: 59
类方法
- 装饰器:
@classmethod
, 第一个参数必须是cls
,表示当前类
class Student:count = 0 # 类属性def __init__(self, name):self.name = nameStudent.count += 1@classmethoddef print_count(cls):print(f"总共有 {cls.count} 个学生")s1 = Student("Bart")
s2 = Student("Lisa")
Student.print_count() # 总共有 2 个学生
静态方法
-
作用:既不操作对象属性,也不操作类属性。
-
装饰器:
@staticmethod
, 可以看作是类里面的普通函数,用类名或对象都能调用
class Math:@staticmethoddef add(x, y):return x + yprint(Math.add(3, 5)) # 8
访问限制
如果要让内部属性不被外部访问,可以在属性的名称前加两个下划线__
,就变成了一个私有变量
class Man: def __init__(self,name,age): self.__name=name self.__age=age def printInfo(self): print(self.__name+str(self.__age)) def getName(self): return self.__name def getAge(self): return self.__age
判断一个变量是否是某个类型可以用isinstance()
isinstance(a, list)
isinstance(1, int)
继承
class Parent:def __init__(self, name):self.name = namedef greet(self):print(f"Hello, I am {self.name}")# 子类继承父类
class Child(Parent):passc = Child("Alice")
c.greet() # Hello, I am Alice
异常处理
try…except
try:r = 10 / 0
except ZeroDivisionError as e:print('捕获到错误:', e)
-
try
:放置可能出错的代码 -
except
:捕获指定类型的异常 -
可以捕获多个异常:
try:r = int('abc') # ValueError
except ZeroDivisionError:print('除零错误')
except ValueError:print('值错误')
else
会在 没有异常发生时执行:
try:r = 10 / 2
except ZeroDivisionError:print('除零错误')
else:print('没有错误,结果:', r)
finally
无论是否发生异常都会执行,常用于释放资源:
try:f = open('test.txt')data = f.read()
except FileNotFoundError:print('文件不存在')
finally:print('关闭文件')f.close()
raise 抛出异常
- 用于 主动抛出异常,可以中断当前流程,让调用者处理。
def foo(x):if x == 0:raise ValueError("x 不能为 0")return 10 / xfoo(0) # 会抛出 ValueError
- 异常是对象,必须是 Exception 的子类。
logging 模块
logging
用于 记录日志,比print
更专业,适合生产环境。
import logginglogging.basicConfig(level=logging.INFO) # 配置日志等级logging.info("这是普通信息")
logging.warning("这是警告信息")
logging.error("这是错误信息")
-
日志等级从低到高:
DEBUG < INFO < WARNING < ERROR < CRITICAL
-
可以把日志输出到文件或终端。
将日志输出到文件
import logging # 创建 logger 对象
logger = logging.getLogger()
logger.setLevel(logging.INFO) # 创建文件处理器
file_handler = logging.FileHandler('app.log', mode='w', encoding='utf-8') # 创建控制台处理器
console_handler = logging.StreamHandler() # 设置统一格式
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter) # 将处理器添加到 loggerlogger.addHandler(file_handler)
logger.addHandler(console_handler) # 写日志
logger.info("这是普通信息")
logger.warning("这是警告信息")
logger.error("这是错误信息")
- 可以配置时间、日志等级、输出格式, 日志可长期保存,用于排查问题
with语句
用于 自动管理资源(比如文件、网络连接、锁等),避免手写繁琐的 try...finally
with open('data.txt', 'r') as f:data = f.read()print(data)
等价于:
f = open('data.txt', 'r')
try:data = f.read()print(data)
finally:f.close()
__name__
是什么
在每个 Python 文件中,解释器都会自动定义一个变量:它表示当前模块的名字。
__name__
-
如果这个文件是 被直接运行的脚本,那么:
__name__ == '__main__'
-
如果这个文件是 被其他模块导入的,那么:
__name__ == 模块名(文件名,不含 .py)
作用:
def main():print("程序的主入口")if __name__ == '__main__':main()
✅ 只有在该文件被“直接运行”时才会执行 main()
❌ 如果该文件被“import 导入”,不会执行。
文件操作
打开文件:open()
f = open("test.txt", "r", encoding="utf-8")
参数说明:
参数 | 作用 |
---|---|
文件名 | "test.txt" 要打开的文件名 |
模式 | "r" 代表读取模式(read) |
编码 | encoding="utf-8" 解决中文乱码问题 |
常见的打开模式:
模式 | 含义 |
---|---|
'r' | 只读模式(默认) |
'w' | 只写模式(会覆盖原内容) |
'a' | 追加模式(在原内容后面追加) |
'rb' | 以二进制方式读取(read binary) |
'wb' | 以二进制方式写入(write binary) |
读取文件内容
f = open("test.txt", "r", encoding="utf-8")
content = f.read() # 读取全部内容
print(content)f.seek(0) # 将文件指针移动到开头
line1 = f.readline() # 读取一行
lines = f.readlines() # 读取所有行,返回列表
f.close()
注意:
使用完文件后必须调用 f.close()
关闭,否则可能导致资源未释放或数据未保存。
写入文件
f = open("output.txt", "w", encoding="utf-8")
f.write("Hello, Python!\n")
f.write("写入第二行")
f.close()
如果文件不存在,
"w"
模式会创建一个新文件。
如果文件存在,内容会被清空后写入。
推荐写法:with open()
(自动关闭文件)
with open("test.txt", "r", encoding="utf-8") as f:for line in f:print(line.strip())
with
块结束后,Python 会自动关闭文件,不需要 f.close()
, 推荐用这个
常见面试题
测试面试题
软件测试的过程
-
需求分析
-
制定测试计划
-
设计测试用例, 搭建测试环境
-
执行测试用例
-
跟踪缺陷,执行回归测试
-
汇总测试结果,写出测试报告
测试用例一般包含哪些内容?
一共八个
- 用例编号:项目_模块_编号
- 标题:预期结果(测试点)
- 模块 / 项目:所属项目或模块
- 优先级:表示用例的重要程度或者影响力 P0 ~ p4 (P0 最高)
- 前置条件:要执行此条用例,有哪些前置操作
- 测试步骤:描述操作步骤
- 测试数据:操作的数据,没有的话可以为空
- 预期结果:期望达到的结果
测试用例的设计方法
-
等价类划分法:将输入划分为有效 / 无效等价类(如手机号测试:有效 11 位数字,无效 < 11 位 / 非数字),减少用例数量
-
边界值分析法:重点测试边界(如微信消息最大长度为 2000 字,测试 1999、2000、2001 字)
-
场景分析法:模拟用户在真实业务场景下的操作流程,来设计测试用例的方法(比如电商 “下单 - 支付 - 发货” 全流程)
-
因果图法:根据多条件组合来设计测试用例(比如如登录时 “用户名 + 密码” 的正确 / 错误组合)
-
错误推测法:基于测试人员的经验、历史缺陷数据、用户使用习惯,推测系统可能出现错误的场景
设计测试用例时要注意什么?
-
覆盖全面:要覆盖功能需求、非功能需求、边界条件、异常场景等
-
可执行性:步骤清晰、预期结果要提示明确
-
可维护性:需求变更时,用例要容易修改
-
符合业务场景
-
独立性:每个用例专注于一个场景,不能依赖其他用例的执行结果
-
简洁性:步骤不能冗余,避免重复
软件测试的原则
-
要尽早开始测试, 降低修复的成本, 越早发现 bug 成本越低
-
穷尽测试是不可能的, 不可能覆盖所有场景进行测试
-
测试依赖于上下文, 不同类型的软件有不同测试方法
-
缺陷集群效应: 大多数缺陷往往集中在少数模块里
-
测试只能显示缺陷存在,不能保证系统完全正确
测试时候遇到不可复现的bug怎么办?
- 按照原有步骤重新测试, 如果 bug 没有出现,做好记录
- 检查 bug 出现时的操作流程和环境配置,判断是不是因为误操作或环境异常导致
- 可以查看日志来分析
- 更换不同的测试环境来复现问题,排除环境因素的影响
- 在后续的测试过程中关注这个bug是否出现
如果出现了bug, 你和开发说,开发说不是bug你怎么解决?
-
重新核对需求文档或接口文档,看双方是否存在对需求的认知偏差
-
把测试过程、输入输出、截图、日志等整理出来,拿给开发看
-
参考需求文档/产品文档,看和需求是否相符
-
可以组织 开发+测试+产品 一起评审, 确定是不是bug
软件测试的目的是什么?
-
发现缺陷,保证软件质量
-
验证软件是否满足需求
-
降低风险,提高用户体验感
黑盒测试与白盒测试的区别是什么?
黑盒测试:是功能性测试,不了解代码结构的前提下,根据功能设计用例来测试
白盒测试:是结构性测试,知道代码逻辑的前提下,根据代码逻辑设计用例,进行用例覆盖
你不是开发吗,为什么选择走测试方向?
我觉得测试是和开发同样重要的岗位。开发是“创造”,测试是“守护”。 在学习的过程中,我发现我对发现问题、分析问题更感兴趣,而不是写功能。同时我有开发的基础,可以更好的理解代码逻辑、写自动化脚本,提升我测试的效率, 所以我选择测试方向
bug的严重程度和优先级
严重程度: 致命,严重,一般,轻微
优先级: 立即解决,高优先级,正常排队,低优先级
和开发说有 bug,bug没改完, 项目快上线了怎么办
先确认 Bug 严重程度和影响范围,然后把复现步骤、截图和日志整理清楚,及时跟开发和产品沟通,说明风险。
如果影响核心功能,建议延期上线或临时规避,否则记录好, 并在下一版本尽快修复,同时提醒上线团队注意。
提Bug要注意哪些点?
提Bug: 问题描述要清晰、Bug的复现步骤要清晰, 环境信息要完整、定位明确、严重性和优先级评估合理、避免重复
从发现到解决 Bug的流程
在执行用例时发现问题,会先复现确认,排查是不是环境或数据导致的。
确认是 Bug 后,就在缺陷管理平台提交,写清楚环境、重现步骤、预期和实际结果、日志和截图。
然后由开发和产品一起评估严重度和优先级,开发定位问题并修复,修复后提交代码和单元测试。
接着测试人员会验证修复情况,并做相关功能的回归测试。
验证通过后就关闭缺陷,如果是高严重度问题,还会做复盘,总结原因,补充测试用例,防止再次发生。
如何确定是不是bug?
先对照需求文档和测试用例,如果和预期不符,可以认定为 Bug。
看下这个问题是否违反了用户的操作习惯,或者行业的通用规范,如果违反了,就是bug
可以找产品经理或者开发人员沟通确定是否为bug
对于需求本身有歧义的情况,可以组织相关人员开会,确认是不是 Bug
以及如何判断一个Bug是前端还是后端引起的?
看 bug 的表现:
-
前端问题:页面样式错乱、交互时没有反应、浏览器控制台报错、请求参数错误
-
后端问题:接口返回 500/502/503,返回数据缺失或错误,响应速度慢,数据结构不对
用浏览器开发者工具看请求有没有发出,响应是否正确
用接口工具比如 postman 调接口,看请求响应是否正确
还可以看前后端代码的日志
你觉得测试最重要的能力是什么(核心竞争力)
我觉得软件测试最重要的能力是发现和分析问题的能力。
测试的核心价值就是在产品上线前发现潜在的问题,保证质量。
这需要有敏锐的观察力,能从用户角度去思考,捕捉到系统中的异常点;同时还要有逻辑思维,能够通过日志分析、用例设计等方法定位问题根源
除此之外,还需要良好的沟通能力,把发现的问题准确、清晰地反馈给开发团队,推动问题解决。
没有需求文档,怎么开展测试
可以找相关人员进行沟通,获取需求,比如产品经理和开发人员
可以根据用户的使用习惯和行业规范来总结需求
可以参考类似的产品来总结需求
接口参数怎么设置
主要包括:
Path 参数:URL 路径中的变量,如 /user/{id}
Query 参数:URL 查询字符串,如 ?page=1&size=10
Header 参数:如 Content-Type: application/json、Authorization: Bearer token
Body 参数:POST/PUT 请求中发送的数据,通常 JSON 或 form-data
pytest 和 unittest的区别
unittest 是 Python 内置的单元测试框架,规范统一,不过语法较繁琐,扩展性弱
pytest 是第三方库, 更轻量、更灵活,扩展性更高, 更适合复杂项目
什么是冒烟测试?
冒烟测试是对新构建的软件进行一次核心功能的快速检查, 确保系统能正常启动、主要功能能跑通,
目的是验证这个版本是否具备进一步测试的条件; 如果冒烟测试失败,就不进行后续的测试
性能测试怎么做?需要关注哪些指标?
-
性能测试流程
- 进行需求分析:明确测试目标与关键指标,确定测试范围
- 搭建测试环境,准备测试数据
- 编写脚本,设置参数等,模拟用户操作
- 执行压测:分阶段增加并发用户,监控服务器资源及中间件状态
- 分析结果:对比实际指标与阈值,找出性能瓶颈
- 优化:根据瓶颈点进行优化,再次回归验证
2.关注的指标:
响应时间、吞吐量、资源利用率,持续运行能力等
打开网页,要响应10秒,你觉得可能的原因
- 用户端网络质量差
- DNS 解析延迟
- 服务器处理慢
- 资源体积太大,比如图片, 或者数量过多
- CDN 故障或配置问题
你了解 Postman 吗?是干啥的?除了接口测试功能还有哪些?
主要功能是 发送HTTP 请求,实现接口功能测试
其他功能:
-
集合测试(一组 API 请求的集合), 可以将接口按业务场景分组,一键运行整个集合, 还支持批量执行与自动化
-
Mock 服务:就是在后端接口未完成的时候, 可模拟接口返回数据, 提供给前端测试
-
文档生成:自动生成接口文档,还支持分享协作
-
环境与变量管理, 支持多环境(比如开发 / 测试 / 生产)配置,可以通过全局变量实现参数传递, 比如用户 token
-
监控功能, 可以对接口设置监控,出现异常可以提示
Linux 查看日志上下五行的命令
用grep
结合上下文参数:
grep -C 5 "关键词" 日志文件
:显示匹配行及上下各 5 行(C=Context)。- 其他常用:
-A 5
(后 5 行,A=After)、-B 5
(前 5 行,B=Before)。
示例:grep -C 5 "error" /var/log/app.log
(查看 app.log 中含 “error” 的行及上下 5 行)。
你对ai的了解 你常用哪个ai 哪个好用?
我对 AI 的理解是:它能够大幅度提高效率,比如在代码开发、文档编写、数据分析等方面。主要用过 ChatGPT 和 豆包
ChatGPT 在学习和解决问题上很有帮助,比如快速理解新知识、生成示例代码;而 豆包 的中文处理能力更突出,知识运用与数学能力更好
我觉得不同 AI 各有优势,如果是学习和技术交流,ChatGPT 更好用;如果是日常沟通和知识处理,豆包更合适。
我觉得 AI 好用的点是,能帮助我们快速定位问题和提供思路,但最后的逻辑和实现还是需要我们自己把控。
常用的 Docker 命令?
- docker pull 拉取镜像
- docker create 创建容器
- docker rm 删除容器
- docker ps 列出正在运行的容器列表
- docker run 创建容器并运行指定命令
- docker start 启动容器
- docker stop 停止运行容器
- docker restart 重启容器
- docker rm 删除容器
- docker exec 容器执行指定命令
- docker rmi 删除镜像
镜像和容器是什么?有什么区别?
镜像是一个 只读的模板,包含了运行应用需要的所有内容:比如操作系统环境、应用程序、依赖库、配置等。镜像是静态的,容器是动态的,镜像不能直接运行
容器是 镜像运行起来后的实例,带有运行时环境,可以启动、停止、删除。
Docker 有哪些优缺点?
优点:
部署方便
隔离性好
轻量级、启动快
缺点
对系统内核依赖强: Docker 容器共享宿主机内核
运维复杂度提升:大规模容器化场景,需要镜像管理、版本管理、监控、日志收集,否则容易失控
selenium定位元素的8个方法是什么?
Selenium 一共有 8 种常用的元素定位方法:id、name、class_name、tag_name、link_text、partial_link_text、xpath、css_selector。
定位不到元素的原因
1、可能是页面加载延迟,这个需要通过等待延迟的方式来处理
2、不过有时候,页面加载完成,但是元素暂时还不可见,导致定位不成功
这个可以选择使用显示等待来处理,可以用 WebDriverWait
类来实现
3、还有就是像内嵌网页的问题,需要使用 driver.switch_to.frame (name/index)
这个函数来跳转到处理。
4、还有要注意多窗口问题,动态 id 问题等的问题,对于多窗口处理,可以使用 driver.switch_to.window ()
的方式来进行处理,而对于动态 id
的问题,需要注意的是有些 id 跟数字有关,可能会动态变化,可以使用 xpath 也可以使用 css_select 属性定位或者样式定位,或者可以通过父元素来找元素,或者通过兄弟节点来找对应的元素。等等
5、还有要特别注意滚动条的问题,这里通过调用 js 代码来实现的,driver.execute_script (js)
6、再这就是有时候会碰到某些元素的是不可见的,比如 display 属性为 none 这就需要通过 java Script 修改 display 的值。
元素定位,有时候定位得到,有时候定位不到,可能是什么原因,你会怎么处理?
1、可能是网络问题,导致页面加载延迟,这个可以做延迟等待,一般选择隐式等待,在脚本前面加 上 driver.implicitly_wait(20)
。
2、也有可能是页面结构发生变化导致的,这个时候最好选择通过 xpath或css结合属性进行或者样式定位,或者采用 JQuery定位的方式来进行定位元素
Vue/React 动态渲染的元素怎么定位?
-
使用显式等待,保证元素加载完成再操作;
-
寻找稳定的属性,比如 data-testid 或 data-* 作为定位标识;
-
可以使用xpath通过元素的文本内容定位
-
通过层级关系定位
网络面试题
HTTP 和 HTTPS 的区别
http是明文传输,容易被窃听、篡改和伪造。https是加密传输,可以防止攻击
http默认的是80端口, https是443端口
传输敏感信息一般用 HTTPS
HTTP/1.0、HTTP/1.1、HTTP/2.0、HTTP/3.0 的区别?
HTTP/1.0 → 是短连接,每次请求都要建立 TCP,效率低
HTTP/1.1 → 是长连接,一个TCP连接可以发送多次请求,但有队头阻塞问题
HTTP/2.0 → 实现了多路复用,一个TCP连接可以处理多个请求, 解决了1.1的请求队头阻塞,但底层仍存在TCP的队头阻塞,引入了二进制传输 + 头部压缩
HTTP/3.0 → 基于 QUIC,使用UDP代替TCP, 彻底解决 TCP 队头阻塞,支持 0-RTT,更快更安全。
HTTP 常见状态码有哪些?
200 OK:请求成功。
201 Created: POST 新建资源成功。
301 永久重定向:资源位置永久改变,浏览器会缓存
302 临时重定向:临时跳转,常见于登录后跳转
400 Bad Request:请求参数错误
401 Unauthorized:未认证或 token 无效
403 Forbidden:认证了但没有权限
404 Not Found:资源不存在
500 Internal Server Error:服务端程序错误。
502 Bad Gateway:网关/代理收到无效响应(常见于 Nginx)
HTTP 请求头中包含什么?
大体分为四类:
通用信息(Host、User-Agent、Accept 等)
缓存与来源控制(Cache-Control、Referer)
认证与安全(Authorization、Cookie)
以及请求体相关(Content-Type、Content-Length)
HTTP 是基于 TCP 还是 UDP?
HTTP/1.x , HTTP/2 都基于 TCP;而 HTTP/3 基于 UDP
HTTP 长连接 vs. 短连接的区别是?
短连接:请求一次建立一次连接,用完就断;
长连接:多个请求复用一个连接,性能更好
从「敲下一个 URL」到「页面出现在屏幕」整条链路全景
从输入 URL 开始,浏览器会先检查缓存,再进行 DNS 解析得到服务器 IP,通过 TCP(三次握手)和 TLS(若 HTTPS)建立连接,发起 HTTP 请求。服务器接收到请求后处理业务逻辑并返回响应,浏览器解析响应数据,构建 DOM 和 CSSOM,生成渲染树,经过布局、绘制和合成,最终由 GPU 将页面呈现到屏幕上。 然后断开链接 四次挥手
GET 与 POST 有什么区别
GET 用于获取数据,参数在 URL,幂等且可缓存;
POST 用于提交数据,参数在请求体,非幂等,不易缓存。
GET 适合查询,POST 适合创建/更新资源。
幂等(多次请求结果相同,不改变服务器状态)
非幂等(多次提交可能创建多条数据或触发多次操作)
HTTP vs. HTTPS 有什么区别?
HTTP以明文形式传输,传输的数据可能会被窃听、篡改、仿造。
HTTPS在 HTTP 上加了 TLS/SSL 加密。更安全, 适合敏感信息传输
WebSocket 简介 & 与 HTTP 的核心区别
WebSocket 是基于 TCP 的全双工长连接协议,建立连接后客户端和服务器可以互相发送数据,适合实时高频场景。
HTTP 是单向请求-响应协议,客户端必须先发起请求,服务器才能响应。
WebSocket数据传输开销更小,效率更高。”
TCP 与 UDP 的区别?
TCP 是面向连接、可靠的传输协议,保证数据完整、顺序、无重复,但开销大,适合需要可靠传输的场景, 比如文件传输, 远程登录
UDP 是无连接、不可靠协议,开销小、传输快,适合对实时性要求高、能容忍少量丢包的场景, 比如视频直播、在线游戏、语音通信
TCP 的三次握手
第一次握手:客户端向服务器端发送SYN,确认客户端的发送没问题。
第二次握手:服务器向客户端发送SYN和ACK包,确认服务器的发送与接收没问题
第三次握手:客户端最后回复一个ACK包,确认客户端的接收没问题。
数据包在网络中传输的过程
-
发送方将应用数据封装成数据包,添加源地址,发送方法,传输协议类型,目的地址等
-
选择路由转发数据包,发送到目标主机
-
接收端解密数据包,恢复成原始数据
ip地址的划分
IP地址主要分为 IPv4 和 IPv6,日常使用更多的是 IPv4。IPv4 通过“网络位”和“主机位”来区分,用于实现网络分层管理和设备寻址。
IPv4 按类别划分为 A/B/C/D/E 类:
- A 类(1.0.0.0–126.255.255.255)用于大型网络,网络位 8 位,主机位 24 位;
- B 类(128.0.0.0–191.255.255.255)用于中型网络,网络位 16 位,主机位 16 位;
- C 类(192.0.0.0–223.255.255.255)用于小型网络,网络位 24 位,主机位 8 位;
- D 类用于组播,E 类为保留地址。
现代网络更常用 无类别域间路由,通过“IP 地址/子网掩码长度”表示,例如 192.168.1.0/24,其中前 24 位为网络位,后 8 位为主机位,可灵活调整网络和主机数量,提高地址利用率。
数据库
1.聚集索引的数据和索引放在一起,一个表只能有一个聚集索引,适合范围查询
非聚集索引的数据和索引分离,索引指向数据,一个表可以有多个非聚集索引,适合精确查询
2.B+ tree的优势(和哈希表,二叉树对比):高度低,磁盘io次数少; 查询高效,而且效率稳定,时间复杂度低
3.索引是一种数据结构,可以提高检索效率,降低io成本,减少cpu消耗
MySQL什么字段适合建立索引?什么不适合?
✅ 适合建立索引的字段
• 经常作为 查询条件(WHERE) 的字段
• 经常用作 排序(ORDER BY)、分组(GROUP BY)、连接(JOIN) 的字段。
• 区分度高的字段,比如身份证号、手机号。
• 频繁被访问 的字段。
❌ 不适合建立索引的字段
• 数据重复度高(如性别、是否删除标志位)。
• 很少用在查询条件中 的字段。
• 频繁更新的字段(会增加索引维护成本)。
• 大文本字段(如 TEXT、BLOB)。
4.索引的缺点:执行增删改操作时效率变低,占用额外存储空间
5.索引失效的情况:模糊匹配的时候以%开头; 对列进行函数运算或表达式计算;字符串不加引号; or连接的条件,一边有索引一边无索引;
6.唯一索引:加速查询 + 列值唯一(可以有 NULL)。
联合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并
索引类型(按照物理结构):聚簇索引、非聚簇索引。
索引类型(按照功能分类):主键索引、唯一索引、全局索引、复合索引、普通索引。
7.事务四大特性(acid): 原子性(回滚日志实现),一致性(通过其他三者实现),持久性(重做日志实现,隔离性(锁机制和mvcc实现)
8.事务隔离级别: 读未提交,读已提交,可重复读,串行化; mysql是默认可重复读
9.mysql默认存储引擎:innodb,综合处理能力和性能最好
innodb支持事务,myisam,memory不支持
innodb支持行级锁和表锁,其他两个只有表级锁
innodb支持外键,其他的不支持
innodb有崩溃恢复机制,其他的没有
innoDB适合高并发写操作;myisam适合读多写少的场景 memory用内存存储数据,访问速度快但数据容易丢失
10.EXPLAIN 命令可以分析 SQL 的 执行计划
11.mysql连接分为内连接和外连接,
内连接只返回两张表中都匹配的记录;
外连接分为左外连接和右外连接:
左外连接返回左表所有行,右表没匹配到的记录用null填充,
右外连接返回右表所有行,左表没匹配到的记录用null填充
12.count(1)、count() 与 count(列名) 的区别?:count(1)、count() 都是统计所有行数,COUNT(列名)会忽略值为null的行
13.覆盖索引是什么?: 查询的所有字段都能从索引本身获取,不需要回表。可以减少一次主键查询,性能更高
14.回表是什么: 通过二级索引(除了主键索引之外的索引)找到主键,再通过主键索引(聚簇索引)查找数据的过程。
15.最左前缀原则: 查询条件必须从索引最左边的列开始匹配,并且连续匹配,索引才会生效. 如果中间某个列不在查询条件中,后面的索引会失效
16.drop、delete 与 truncate 的区别?
DROP 用来删除整张表,包括表结构,不能回滚。
TRUNCATE 用于清空表中的所有数据,但会保留表结构,不能回滚。
DELETE 用来删除行,可以带 WHERE 条件,可以回滚。
17.sql查询的执行顺序: 先执行 FROM 确定主表,再执行 JOIN 连接,然后 WHERE 进行过滤,接着 GROUP BY 进行分组,HAVING 过滤聚合结果,SELECT 选择最终列,ORDER BY 排序,最后 LIMIT 限制返回的行数
-
InnoDB 是如何存储数据的?
InnoDB 的数据按行存储在页(16KB)中,页组成段,段存放在表空间中。
数据通过聚集索引存储,二级索引存储主键引用。
InnoDB 支持事务和 MVCC,使用 Redo Log 和 Undo Log 实现崩溃恢复和事务回滚,同时通过缓冲池加速磁盘读写。 -
Hash 索引与 BTree 索引有什么区别?
Hash 索引基于哈希表实现,支持等值查询,查询速度快,不支持范围查询和排序操作;
BTree 索引基于平衡树结构,支持等值查询、范围查询和排序操作,适合大多数查询场景 -
SQL 聚合函数有哪些?
COUNT、SUM、AVG、MIN、MAX等,用于对数据集进行汇总和统计分析。