软件高级测试之数据库测试篇


前言

当今世界是一个充满着大量数据的互联网世界,充斥着大量的数据。即这个互联网世界就是数据世界。
各种各样的软件应用程序,无论是在Web应用、桌面应用、客户端服务器、企业和个人业务,都需要有自己的数据库在后端操作。随着现在应用的复杂程度增加,应用需要更强大和安全系数高的数据库才可以满足需求。为了满足高频率的应用程序事务(如银行或财务应用),数据库的安全性成为首要之重,今天我们就来介绍下数据库测试~


提示:以下是本篇文章正文内容,下面案例可供参考

一、概念

数据库是存放数据的仓库。它的存储空间一般很大,可以存放百万条、千万条、上亿条数据,但是数据库并不是随意地将数据进行存放,是要有一定的规则的,否则查询的效率会很低。所以应运而生出了数据库测试这个概念。

数据库测试也称为后端测试。数据库测试分为四个不同的类别:

ⅰ. 数据完整性测试
ⅱ. 数据有效性测试
ⅲ. 数据库性能测试
ⅳ. 测试功能,程序和触发器

二、基本测试

数据库发展至今,已不再是单纯的用来存储记录。在确保数据库中包含的数据尽可能地准确和一致的数据性质,也即数据完整性之外,数据库系统比以前具有了更多的强大功能,例如参考完整性,关系约束,触发器和存储过程等。

1. 具备技能

为了测试测试数据库正确和准确性。测试人员需要掌握以下2点基本技能:

  1. 测试人员要熟练掌握SQL和DML(数据库语言)语句
  2. 其次,测试人员应该掌握数据库的结构

2. 测试点汇总

基本的数据库测试,应该从以下4各方面入手:

  • 数据的正确性测试:数据有无乱码、页面的增删改查,数据库是否成功的显示相应的数据
  • 数据库构造:数据库配置文件修改不能否访问数据库、构造异常的数据不能否在页面显示、加密字段是否密文显示
  • 数据同步测试:构造重复的数据看数据库是否能去重、是否有对异常数据的容错处理机制(处理方式建议是正常的数据入库成功,异常的数据入库失败并单独保存在异常数据表中,也同时呈现在页面;)是否有历史数据处理机制;敏感字段的值是否按照要求进行了安全的加密处理
  • 突发突发情况应对测试

附突发情况包含但不局限于一下几种情况:

编号描述
目标数据库停止服务
目标数据库突然断电或重启
源数据在同步过程中网络异常中断
数据入库失败,是否可以快速进行数据回滚以恢复数据
目标表被锁时,解锁机制是否易用

三、容量测试

随着数据库系统的使用,数据量在飞速增长,如何在使用前对数据容量的增长情况进行初步估算,为最终用户提供参考,这在数据库使用和维护过程中,是非常重要的。

可以通过对数据库设计中基本表的数据大小,和每天数据表的数据产生量进行初步估算。

四、性能测试

目的:发现数据库相关的性能瓶颈
范围:

ⅰ. SQL语句查询(慢查询等)
ⅱ. 资源使用率
ⅲ. 数据库架构的合理性
ⅳ. 数据库性能指标

1. SQL语句的分析与调优

使用explain分析查询:

explain select * from stu where id = 1;

在这里插入图片描述

type,即连接类型
possiable keys:可能用到的索引
key:真正用到的索引
ley_len:索引长度,越短越好
ref:若果是常数等值查询,显示const;如果使用了表达式或函数,显示func
rows:查询的行数

Type的值,从好到差依次为:system 、const 、eq_ref、ref、fulltext、ref_or_null、unique_subquery、index_subquery、range、 index_merge、index、ALL
除了ALL,其他的type都可以用到索引,扫描全表数据文件;除了index_merge外,其他只能用一个索引,常见于and或or使用了不同的索引

  • system:表中只有一行或是空表,且只能用myisam和memory表
  • const:使用唯一索引或主键返回记录一定是1行记录的等值where条件时,通常type是const
  • eq_ref:出现在要连接过这个表的查询计划中,驱动表只返回 一行数据,且这行数据时第二表的主键或者唯一索引
  • ref:返回数据不唯一的等值查找就肯能出现
  • fulltext:全文检索,优先级很高
  • index:索引全表扫描,把索引从头到尾扫描一遍
  • ALL:表示全表扫描,若表中数据有百万至千万级别,必须要优化,否则性能很慢;

2. MySql数据库监控指标

  1. QPS

queries per seconds 每秒查询数量
获取方法:mysql> show global status like ‘Quesion%’
计算方法:Queries /seconds

  1. TPS

Transaction per seconds 每秒事务数
计算方法:TPS = (Com_commit + Com_rollback) / seconds
mysql> show global status like ‘Com_commit’;
mysql> show global status like ‘Com_rollback’;

  1. 线程连接数

获取最大使用的连接数:mysql> show global status like ‘Max_used_connections’;
获取线程数:mysql> show global status like “Threads%”

  1. 最大连接数

获取最大的连接数:mysql> show variables status like ‘max_connections’;

  1. Query Cache

查询缓存用于缓存select查询结果,当下次收到相同查询请求时,不在执行知己查询处理而直接返回结果;适用于大量查询、很少改变表中数据
开启Query Cache方法:修改my.cnf文件,将query_cache_size设置为具体大小,具体大小是多少取决于查询的实际情况,最好设置为1024的倍数(参考值为32M)
query_cache_type=0/1/2(1:缓存所有结果;2:指定缓存)

  1. Query Cache命中率

获取:mysql> show status like “Qcache%”;
计算:Query_cache_hits = (Query_hits / (Qcache_hits + Qcache_inserts)) * 100%;

  1. 锁定状态

获取:mysql> show global status like ‘%lock%’;
计算:Table_locks_waited / Table_locks_immediate 值越大代表表锁造成的阻塞越严重
nnodb_row_lock_waits innodb行锁,太大可能是间隙锁造成的

  1. 主从延时

查询:mysql> show salve status;

3. DataFactory

DataFactory 是一种强大的数据产生器,它允许开发人员和测试人员很容易产生百万行有意义的正确的测试数据库,该工具支持DB2、Oracle、Sybase、SQL Server 数据库。

这样,就可以模拟出应用软件长期使用后,海量数据存储的数据库的性能状况。从而尽早发现问题,进行数据库性能的优化。

4. 启用MySql慢查询

执行速度超过定义的时间的查询即为慢查询;不同的系统定义不同的慢查询。
慢查询开启:

//编辑etc/my.cnf ,在[mysqld]域中添加
slow_query_log = 1
//慢查询日志路径
slow_query_log_file=/var/log/mysql/slow.log
//慢查询的时长
long_query_time=1
//未使用索引的查询也被记录到慢查询日志中
log_queries_not_using_indexes = 1

慢查询日志分析

mysqldumpslow
//参数说明:
// -s 表示按何种方式排序
// c:访问计数
// l:锁定时间
// r:返回记录
// t:查询时间
// al:平均锁定时间
// ar:平均返回记录数
// at:平均查询时间
// -t 返回前面多少条的数据
// -g 后面写一个正则匹配模式,大小写不敏感

// 返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 slow.log
//返回访问次数最多的10个SQL
mysqldumpslow -s c -t 10 slow.log
//返回按照时间排序的前10条里面包含左连接的SQL
mysqldumpslow -s t-t 10 -g “left join” slow.log

5. 添加使用MySQL索引

  • 主键索引: primary key 不能重复,主键至多只有一个,创建表时确定创建
  • 唯一索引: unique index 行上的值不能重复,允许有空值
  • 普通索引:index 仅仅是加快查询速度
  • 全文索引: fulltext index
  • 组合索引: 多列索引,是得多列上创建索引,最左前缀规则(id,name,age–>id、name、age,id、name,id)

6. 添加使用MySQL的存储引擎

在这里插入图片描述

MyISAM
优点:读的性能比Innodb高很多、索引与数据分开,使用了压缩,提高了内存使用率
缺点:不支持事务、写入数据时,直接锁表。

InnoDB
优点:支持事务、支持外键、支持行级锁
缺点:不支持全文索引、行级锁并不绝对,当不确定扫描范围时,锁全表、索引与数据紧密捆绑,没使用压缩导致体积庞大。

五、压力测试

数据库在大多数软件项目中是不可缺少的,对于它进行压力测试是为了找出数据库对象是否可以有效地承受来自多个用户的并发访问。
这些对象主要是:索引、触发器、存储过程和锁。

通过对SQL语句和存储过程的测试,自动化的压力测试工具可以间接的反应数据库对象是否需要优化。

六、安全测试

此处,我们重点说下SQL注入测试,其一般遵循下面步骤:

  1. 测试注入类型,数字型or字符型
  • 如果参数中直接包含字母,那么直接可以判断是字符型参数,如id=4a。
  • 若参数是数字通常可以考虑输入表达式来判断,如id=6,可尝试输入id=7-1或id=3*2
  • 如果返回结果和id=6相同,可以确认为数字,进行2. 逻辑判断
  • 若返回空,可进一步测试是否为字符型或是否有过滤。在参数后加单引号,如id=6’
  • 若出现了数据库报错,那么极大可能存在字符型sql注入,直接进行2.逻辑测试。
  • 如果返回空或正常,则进行4. 过滤判断
  1. 测试逻辑语句
  • 以逻辑真和逻辑假共同测试,形如:
    id=6’ and 1=1 –
    id=6’ and ‘1’=‘1
    id=6’ and 1=2 –
    id=6’ and ‘1’='2
    //如果是数字型注入,那么id=6’后的单引号省略
  • 根据实际情况来判断是否需要拼接语句,拼接方法也因地制宜
  • 若逻辑真返回结果和id=6结果相同,逻辑假返回空,则可以判断存在sql注入
  • 若逻辑真和逻辑假返回结果都和id=6结果相同或都为空,则进行4. 过滤判断
  • 若过滤判断成功绕过过滤,但也没有返回符合逻辑真假的结果,则不存在sql注入或进行3. 延时判断
  1. 测试延时语句
  • 在一些情况下,可根据个人判断,假如一个页面的返回结果可能和参数无关,也就是说,参数的查询结果或语句报错都不会影响页面的返回的情况下,可以尝试延时注入,如id=6 and sleep(5) –
  • 若成功延时返回,则说明存在sql注入
  • 若没成功延时,则进行4. 过滤判断
  • 过滤测试若成功绕过,也没有触发延时,则不存在sql注入
  1. 测试有无过滤策略
  • 下面测试仅针对“过滤”,如被“拦截”比较明显,无需测试。首先进行fuzz测试,输入如id=6asidji
  • 若结果和id=6相同,则后台存在数据类型转换或非直接引用参数,不存在sql注入
  • 若返回结果空则进行特殊单词过滤检测,如id=6and,id=6sleep(5),id=6’等
  • 若返回结果和id=6相同,说明目标单词被过滤,尝试替换绕过后继续测试,>- 若找不到绕过方式,怀疑为白名单或无法绕过,可认为不存在sql注入
  • 若返回结果为空,则说明没有被过滤,尝试单词组合,如id=6 and(这句话测试空格和and的组合,还有其他组合,具体组合方式因地制宜,无需构造完整的sql语句)
  • 若返回结果和id=6相同,则被过滤,尝试替换绕过后继续测试,若找不到绕过方式,怀疑为白名单或无法绕过,可认为不存在sql注入
  • 若返回结果为空,则说明不存在过滤,进行更多的组合测试,若没发现过滤,那么结合之前的1. 2. 3.三步测试结果(一定是前三步没有收获才会进行这一步)可得出结论不存在sql注入。
  • 根据实际情况进行http协议层面的绕过,如测试中目标开启了waf,怀疑被waf过滤,那么可尝试http协议绕过。

更多参考文章:

  1. SQL注入测试思路,入门必看!
  2. 一文带你学会数据库测试核心内容和流程
  3. 测试—数据库性能测试
  4. 面试总结分享:25道数据库测试题
  5. 数据库如何测试,怎么进行数据库的测试?
  • 4
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
package com.bjsxt.servlet; import com.bjsxt.entity.User; import com.bjsxt.service.UserService; import com.bjsxt.service.impl.UserServiceImpl; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; import java.net.URLEncoder; import java.sql.Date; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class UserServlet extends BaseServlet { // @Override // protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // //解决POST表单的中文乱码问题 // request.setCharacterEncoding("utf-8"); // //接收method属性的值 // String methodName = request.getParameter("method"); // // //根据method属性的值调用相应的方法 // if("login".equals(methodName)){ // this.login(request,response); // }else if("register".equals(methodName)){ // this.register(request,response); // }else if("logout".equals(methodName)){ // this.logout(request,response); // } // // } public void show(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取表单的数据 String userId = request.getParameter("userId"); if(userId == null){ userId = ""; } String strAge = request.getParameter("minAge"); int minAge = 0; try{ minAge = Integer.parseInt(strAge); //"12" "abc" }catch(NumberFormatException e){ e.printStackTrace(); } //调用业务层完成查询操作 UserService userService = new UserServiceImpl(); //List<User> userList = userService.findAll(); List<User> userList = userService.find(userId,minAge); //List<User> userList = null; //List<User> userList = new ArrayList<User>(); //跳转到show.jsp显示数据 request.setAttribute("userId",userId); request.setAttribute("minAge",strAge); request.setAttribute("ulist",userList); request.getRequestDispatcher("/admin/show.jsp").forward(request,response); } public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //结束当前的session request.getSession().invalidate(); //跳转回登录页面 response.sendRedirect(request.getContextPath()+"/admin/login.jsp"); } public void register(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //request.setCharacterEncoding("utf-8"); //1.接收来自视图层的表单数据 String userId = request.getParameter("userId"); String realName = request.getParameter("realName"); String pwd = request.getParameter("pwd"); String rePwd = request.getParameter("repwd"); int age = Integer.parseInt(request.getParameter("age"));// "23" String [] hobbyArr = request.getParameterValues("hobby"); String strDate = request.getParameter("enterDate");//"1999-12-23" Date enterDate = Date.valueOf(strDate); //util.Date SimpleDateFormat //判断两次密码是否相同 if(pwd == null || !pwd.equals(rePwd)){ request.setAttribute("error","两次密码必须相同"); request.getRequestDispatcher("/admin/register.jsp").forward(request,response); return; } //2.调用业务层完成注册操作并返回结果 User user = new User(userId,realName,pwd,age, Arrays.toString(hobbyArr),enterDate); UserService userService = new UserServiceImpl(); int n = userService.register(user); //3.根据结果进行页面跳转 if(n>0){ response.sendRedirect(request.getContextPath()+"/admin/login.jsp"); }else{ request.setAttribute("error","注册失败"); request.getRequestDispatcher("/admin/register.jsp").forward(request,response); } } public void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //解决POST表单的中文乱码问题 //request.setCharacterEncoding("utf-8"); //获取用户名和密码 request 内建对象 请求 String username = request.getParameter("username"); String password = request.getParameter("password"); String rememberme = request.getParameter("rememberme"); //调用下一层判断登录是否成功,并返回结果 //进行服务器端的表单验证 if(username ==null || "".equals(username)){ request.setAttribute("error","用户名不能为空JSP"); request.getRequestDispatcher("/admin/login.jsp").forward(request,response); return; } if (username.length()<=6){ request.setAttribute("error","用户名长度必须大于6JSP"); request.getRequestDispatcher("/admin/login.jsp").forward(request,response);//后面语句还会执行 return; //后面的语句不再执行 } // boolean flag = false;//默认失败 // if(username.indexOf("sxt")>=0 || username.contains("尚学堂")){ // flag = true; // } User user = null;//默认登录失败 // UserDao userDao = new UserDaoImpl(); // user = userDao.find(username,password); UserService userService = new UserServiceImpl(); user = userService.login(username,password); //userService.addOrder("shoppingCart"); //输出结果 if(user != null){ //登录成功才记住我 //1.办理会员卡 String username2 = URLEncoder.encode(username,"utf-8"); Cookie cookie1 = new Cookie("uname",username2); Cookie cookie2 = new Cookie("password",password); //2.指定会员卡的作用范围,默认范围是当前目录 /servlet/LoginServlet /admin/login.jsp //cookie1.setPath("/"); //当前服务器 cookie1.setPath("/myservlet2/"); //当前项目 cookie2.setPath("/myservlet2"); //3.指定会员卡的作用时间 if("yes".equals(rememberme)){ cookie1.setMaxAge(60*60*24*10); //默认的时间浏览器不关闭的时间;-1 表示一直有效 cookie2.setMaxAge(60*60*24*10); }else{ cookie1.setMaxAge(0); cookie2.setMaxAge(0); } //4.将会员卡带回家 response.addCookie(cookie1); response.addCookie(cookie2); //成功跳转到成功页面 //out.println("登录成功"); // /servlet/LoginServlet // /servlet/success.jsp // request.getRequestDispatcher("/admin/success.jsp").forward(request,response); HttpSession session = request.getSession(); // session.setAttribute("username",username); session.setAttribute("user",user); //response.sendRedirect("/myservlet2/admin/success.jsp"); //response.sendRedirect("https://www.bjsxt.com:443/news/11377.html"); //response.sendRedirect("http://localhost:8080/myservlet2/admin/success.jsp"); //response.sendRedirect("/myservlet2/admin/success.jsp"); //response.sendRedirect("/myservlet2/admin/success.jsp"); //response.sendRedirect(request.getContextPath()+"/admin/success.jsp"); //http://192.168.58.250:8080/myservlet2/servlet/LoginServlet //http://192.168.58.250:8080/myservlet2/admin/success.jsp //登录成功后,网站的访问人数+1 //1.获取当前的访问人数 ServletContext context = this.getServletContext(); Integer count2 = (Integer) context.getAttribute("count"); //2.人数+1 if(count2 == null){ //第一个用户 count2 = 1; }else{ count2++; } //3.再存放到application作用域中 context.setAttribute("count",count2); //http://192.168.58.250:8080/myservlet2/servlet/admin/success.jsp response.sendRedirect("../admin/success.jsp"); }else{ //失败跳转回登录页面 //out.println("登录失败"); request.setAttribute("error","用户名或者密码错误"); // RequestDispatcher rd = request.getRequestDispatcher("/admin/login.jsp"); // rd.forward(request,response); //RequestDispatcher rd = request.getRequestDispatcher("http://localhost:8080/myservlet2/admin/login.jsp"); //RequestDispatcher rd = request.getRequestDispatcher("/admin/login.jsp"); //http://192.168.58.250:8080/myservlet2/servlet/admin/login.jsp RequestDispatcher rd = request.getRequestDispatcher("../admin/login.jsp"); rd.forward(request,response); } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值