现象:Nginx与应用都在同一台服务器(4g内存、4核cpu)上,nginx缓存区内存配置1g,开启nginx的accesslog,跑图片终端页性能脚本,观察到accesslog里面有90%以上的MISS状态的,nginx缓存没有起到作用,加大nginx缓存内存为2g,清了缓存再次跑性能脚本,accesslog中的MISS状态仍占大部分,且应用服务器的内存空间基本被用完。
解决:将nginx与应用分开,nginx放在一台服务器上,应用包搬到另一服务器(6g内存、8核cpu)上,跑图片终端页脚本,nginx缓存区内存配置2g,观察到响应提上去了,accesslog里HIT状态的占90%或更多。说明nginx缓存区有起到作用。
主要原因:nginx的缓存区设置1G时不够用,没起到作用。当调整到2G时,由于服务器上还存放应用也占了内存,另外系统也需要资源,导致nginx所配置的2G内存没起作用。当把nginx和应用分开时,资源都充足了,这时nginx的缓存区也能起到作用。
0. 基本命令
linux 基本命令整理
1. 压缩 解压
tar -zcvf a.tar.gz a #把a压缩成a.tar.gz
tar -zxvf a.tar.gz #把a.tar.gz解压成a
2. vim小结
2.1 vim替换
:m,ns/word_1/word_2/gc #把word_1用word_2替换,g表示替换所有的, c表示替换每一个时需要确认
2.2 vim统计某一个字符串的个数
:m,ns/word_1/&/gn #统计从m行到n行之间word_1的个数, n表示只是统计个数不替换
:1,$s/word_1/&/gn #搜索整个文档中word_1的个数,和下面等价
:%s/word_1/&/gn
2.3 vim中删除某一字符串
:m,ng/word_1/d #从第m行到第n行删除所有的word_1
3. 文件搜索
3.1 locate——通过文件名查找
locate /bin/zip
3.2 find——通过文件的各种属性在既定的目录下查找
find /usr -type f -name "*.png" -size +1M #查找的目录范围是/usr,名字以.png结尾,大小大于1M(+1M,1M,-1M)
find /usr -type f -name "*.png" -size +1M | wc -l #统计符合条件的行数
find /usr -type f -name "*.png" -size +1M -delete #删除符合条件的
3.3 找出目录dirs下含有字符串“hello”的所有文件的名字(个数)
find .|xargs grep -ri "
IBM" #xargs是一条Unix和类Unix操作系统的常用命令。它的作用是将参数列表转换成小块分段传递给其他命令,以避免参数列表过长的问题。
find .|xargs grep -ri "IBM" -l #只打印出文件名
4. 排序
cat file_name | sort -k2 -r #按第二列(从一开始技术)排序,-r表示reverse,从大到小输出
cat file_name | sort -k1 -n #按第一列排序, -n按数字排序,默认为按字符串排序
cat file_name | sort -k1 -nr | wc -l #统计满足条件的个数
5. 系统开销
5.1 df——磁盘占用情况
df #列出各文件系统的磁盘空间占用情况(已用 未用)共五列:Size Used Avail Use% Mounted on
df -h #以更易读的方式显示 (按K\M\G适当转换)
5.2 du——文件大小
df #列出本目录下,目录的大小(默认的计数单位是k)
df -h 文件名 #以更易读的方式显示所查文件的大小
5.3 w——CPU负载度量(简单的说是进程队列的长度,最近一段时间1min,5min,15min的load度量)
w
6. awk命令
cat file_name | awk '{print $1}' #输出第一列(默认以空格切分)
cat file_name | awk -F ':' '{print $1"\t"$3}' #-F指定切割符号,输出第3列
cat file_name | awk -F ':' 'BEGIN {print "name,id"} {print $1","$3} END {print "end_name,end_id"}' #BEGIN指定开头输出,END指出结尾输出
cat file_name | awk -F ':' '/keyWord/{print $1}' # 输出一行中含有关键字keyWord的制定列
cat file_name | awk -F ':' '{print "filename:" FILENAME ",linenumber:" NR ",columns:" NF}' #内置变量FILENAME文件名,NR已读记录数,NF列数
cat file_name | awk '{count++} END {print "Count:" count}' #编程,最后输出总行数
7. 编码转换
iconv -f gbk -t utf-8 -c text.txt -o text.out #-f:from -t:to -c从输出中忽略无效的输出 -o输出文件名字
8. 文件属性
chmod 属性 文件名 #更改文件属性r:1 w:2 x:4
chown 拥有者 文件名
chgrp 组名 文件名
9. 管道 | 重定向 >
ls -l |grep "^-" | wc -l #grep 正则匹配以'-'开头的, wc -l:统计满足条件的总的行数
ls -l |grep "^-" >file_name1 #把满足结果的定位到file_name1,注:先清空再定位
ls -l |grep "^-" >>file_name2 #把满足结果的输出到file_name2的后面,注:不清空,在原来基础上继续存储
10. 文件传输下载
curl http://www.cnblogs.com/kaituorensheng/ #下载网页,默认只下载HTML文档; -l只显示头部; -i 显示全部
curl http://e.hiphotos.baidu.com/image/pic/item/50da81cb39dbb6fd1e165c260a24ab18972b3764.jpg #下载图片
curl "www.hotmail.com/when/junk.cgi?birthyear=1905&press=OK" #获取表单,参数birthyear=1905,press=OK"
1:添加命名空间System.Data.SqlClient中的SQL Server访问类; 2:与SQL Server数据库建立连接,ADO.NET提供Connection对象用于建立与SQL Server数据库的连接 string connectionStr = "Data source=服务器名;Initial Catalog=数据库名称; uid=用户名;pwd=密码()"; // 定义连接字符串 // Integrated Security=True 集成身份验证 //uid=xxx;Pwd=xxx 用户名密码登陆 SqlConnection connection1 = new SqlConnection(connectionStr); ///实例化Connection对象用于连接数据源 connection1.Open(); ///打开 数据库连接 …………… connection1.Close(); ///关闭数据库连接 |
3:与SQL Server数据库建立连接后,使用命令对象SqlCommand类直接对数据库进行操作
(1)增加、删除、修改操作
SqlConnection connection1 = new SqlConnection(connectionStr); //建立连接 connection1.Open(); //打开数据库连接 string sqlStr = "(SQL执行语句,例如 insert into A values('abc',1))"; //定义相关的执行语句,相当于写好命令 SqlCommand command1 = new SqlCommand(sqlStr, connection1); //构造函数指定命令对象所使用的连接对象connection1以及命令文本sqlStr ,相当于让系统接受命令。 command1.ExecuteNonQuery(); //ExecuteNonQuery()方法返回值为一整数,代表操作所影响到的行数,注意ExecuteNonQuery()方法一般用于执行 // UPDATE、INSERT、DELETE等非查询语句,可以理解为让系统执行命令 connection1.Close(); ///关闭数据库连接 |
示例1:删除的Course表中课程编号为003的记录:
string connectionStr = "Data source=.;Initial Catalog=Student; Integrated Security=True"; SqlConnection connections = new SqlConnection(connectionStr); string sqlstr = "delete from Course where Cno='006' "; SqlCommand command1 = new SqlCommand(sqlstr, connectionss); conn.Open(); if (command1.ExecuteNonQuery() > 0) { MessageBox.Show("删除课程成功!"); }; connections .Close(); |
示例2:向Course表中增加一门课程,课程信息由前台输入
string connectionStr = "Data source=.;Initial Catalog=Student;Integrated Security=True"; SqlConnection connection = new SqlConnection(connectionStr); int Credit = Convert.ToInt32(txtCredit.Text); /TextBox.text是string类型,需要用到强制转换方法“Convert.ToInt32”将string类型转化为int类型 string sqlStr = "insert into Course values('" + txtCno.Text + "','" + txtCname.Text + "'," + Credit + ")";//因为字符串的组成部分为需要从前台读取的变量,所以在这里需要用到字符串拼接, //拼接字符:‘ “+字符串变量+” ’,拼接数字:“+数字变量+” SqlCommand command1= new SqlCommand(sqlStr, connection); connection.Open(); if (command1.ExecuteNonQuery() > 0) { MessageBox.Show("课程添加成功!"); }; connection.Close(); |
示例3:把课程“线性代数”的学分修改为5分
string connectionStr = "Data source=.;Initial Catalog=Student; Integrated Security=True"; SqlConnection connection = new SqlConnection(connectionStr); string sqlStr = "update Course set Ccredit=5 where Cname='线性代数'"; SqlCommand command1= new SqlCommand(sqlStr, connection); connection .Open(); if (command1.ExecuteNonQuery() > 0) { MessageBox.Show("学分修改成功!"); }; connection .Close(); |
(2)查询数据库,用ExecuteScalar()方法,返回单个值(Object)(查询结果第一行第一列的值)
示例4:从Student表中查询学号为201244111学生的姓名:
string connectionStr = "Data source=.;Initial Catalog=Student; Integrated Security=True"; SqlConnection connection = new SqlConnection(connectionStr); string sqlstr = "select Sname from student where Sno='201244111' "; SqlCommand command1 = new SqlCommand(sqlstr, connection); connection.Open(); string studentName = command1.ExecuteScalar().ToString(); MessageBox.Show(studentName); connection.Close(); |
使用DataReader读取多行数据,逐行读取,每次读一行
示例5:运用DataReader逐行读出student表中的第一列数据
string connectionStr = "Data source=.;Initial Catalog=Student; Integrated Security=True"; SqlConnection connection = new SqlConnection(connectionStr); string sqlstr = "select *from student"; SqlCommand command1 = new SqlCommand(sqlstr, connection); connection.Open(); SqlDataReader dataReader1 = command1.ExecuteReader(); // DataReader类没有构造函数,不能实例化,需要通过调用Command对象的command1的ExecuteReader()方法 while (dataReader1.Read()) ///DataReader的Read()方法用于读取数据,每执行一次该语句,DataReader就向前读取一行数据;如果遇到末尾,就返回False,否则为True { MessageBox.Show(dataReader1[0].ToString()); } connection.Close(); |
4.使用SqlDataAdapter数据适配器类访问数据库 ,注意:它既可以将数据库中数据传给数据集中的表,又可将数据集中的表传到数据库中。简言之,数据适配器类用于数据源与数据集间交换数据
(链接语句略)
connection1.Open(); ///打开数据库连接
string sqlStr = "SELECT * FROM A"; ///从A表中选择所有数据的SQL语句
SqlDataAdapter dataAdapter1 = new dataAdapter(sqlStr, connection1); ///构造名为dataAdapter1的数据适配器对象, 并指定连接对象connection1以及SELECT语句
DataSet dataSet1 = new DataSet(); ///构造名为dataSet1的数据集对象 dataAdapter1.Fill(dataSet1);
………………………………
///使用SqlDataAdapter类中的Fill()方法将数据填充到数据集中,注意:SqlDataAdapter类中的Fill()方法和Update()方法可用于将数据填充到单个数据表或数据集中
connection1.Close();
示例6:将Student表中的数据全部查询出来
string connectionStr = "Data source=.;Initial Catalog=Student; Integrated Security=True"; SqlConnection connection = new SqlConnection(connectionStr); string sqlstr = "select *from student"; connection.Open(); SqlDataAdapter dataAdapter1 = new SqlDataAdapter(sqlstr, connection); DataSet dataSet1 = new DataSet(); dataAdapter1.Fill(dataSet1); ///使用SqlDataAdapter类中的Fill()方法将数据填充到数据集中,相当于程序的临时数据库 DataTable dt1 = dataSet1.Tables[0]; ///获取数据集的第一张表 this.dataGridView1.DataSource = dt1; connection.Close(); |
包括一个简单的服务器和一个简单的客户端。
运行时,先运行服务器,然后在运行客户端,就可以进行聊天了。
默认的配置是localhost,端口4545,更改ip就可以在两天电脑上进行聊天了。
目前不支持内网和外网之间的访问,也不支持多人聊天。
因为这只是一个简单的例子,感兴趣的同学可以通过改进,实现多人聊天和内外网之间的访问。
效果图:
下载地址:http://download.csdn.net/source/2958843
源代码:
QQServer.java //axun @copy right package axun.com; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class QQServer{ private JFrame f=new JFrame("QQ服务器端"); private JPanel pleft=new JPanel(new BorderLayout()); private JPanel pright=new JPanel(); private List list=new List(); private TextArea t1=new TextArea(); private TextArea t2=new TextArea(); private Button b=new Button("发送"); //一下是 网络通信用的变量 DataOutputStream dos=null; BufferedReader br=null; DataInputStream dis=null; public QQServer(){ f.setSize(400,300); f.setLayout(new BorderLayout()); f.add(pleft,BorderLayout.WEST); f.add(pright,BorderLayout.CENTER); pleft.add(list); pright.setLayout(new GridLayout(3,1)); pright.add(t1); pright.add(t2); pright.add(b); f.setVisible(true); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); b.addActionListener(new bListener()); } public void Addt1(String s){ t1.append(s); } public void addList(String s){ list.addItem(s); } public static void main(String[] args) throws Exception{ QQServer server=new QQServer(); InputStream in=null; OutputStream out=null; String string=null; ServerSocket ss=new ServerSocket(4545); Socket s=null; s=ss.accept(); server.addList(s.toString()); in=s.getInputStream(); out=s.getOutputStream(); server.dis=new DataInputStream(in); server.dos=new DataOutputStream(out); Listen1 l=new Listen1(server,server.dis); Thread t=new Thread(l); t.start(); } class bListener implements ActionListener{ public void actionPerformed(ActionEvent e) { try{ dos.writeUTF(t2.getText()); Addt1("发送:"+"/n"); Addt1(" "+t2.getText()+"/n"); t2.setText(""); }catch(Exception ep){ Addt1("消息发送失败!/n"); } } } } class Listen1 implements Runnable{ private QQServer server=null; private DataInputStream dis=null; private String s=null; Listen1(QQServer server,DataInputStream dis){ this.server=server; this.dis=dis; } public void run() { // TODO Auto-generated method stub try{ while(true){ s=dis.readUTF(); server.Addt1("收到:"+"/n"); server.Addt1(" "+s+"/n"); } }catch(Exception e){ server.Addt1("Error!:"+s+"/n"); } } } |
QQClient.java
//axun @copy right package axun.com; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.awt.*; import javax.swing.*; import java.awt.event.*; public class QQClient { private JFrame f=new JFrame("QQ客户端"); private TextArea t1=new TextArea(); private TextArea t2=new TextArea(); private Button b=new Button("发送"); //一下是 网络通信用的变量 DataOutputStream dos=null; BufferedReader br=null; DataInputStream dis=null; public void Addt1(String s){ t1.append(s); } public QQClient(){ f.setSize(400,300); f.setLayout(new GridLayout(3,1)); t1.setEditable(false); //不可编辑 f.add(t1); f.add(t2); f.add(b); f.setVisible(true); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); b.addActionListener(new bListener()); } public static void main(String[] args) throws Exception { QQClient client=new QQClient(); InputStream in=null; OutputStream out=null; String string=null; Socket s=new Socket("localhost",4545); out=s.getOutputStream(); in=s.getInputStream(); client.dis=new DataInputStream(in); client.dos=new DataOutputStream(out); Listen2 l=new Listen2(client,client.dis); Thread t=new Thread(l); t.start(); } class bListener implements ActionListener{ public void actionPerformed(ActionEvent e) { try{ dos.writeUTF(t2.getText()); Addt1("发送:"+"/n"); Addt1(" "+t2.getText()+"/n"); t2.setText(""); }catch(Exception ep){ } } } } class Listen2 implements Runnable{ private QQClient client=null; private DataInputStream dis=null; private String s=null; Listen2(QQClient client,DataInputStream dis){ this.client=client; this.dis=dis; } public void run() { // TODO Auto-generated method stub try{ while(true){ s=dis.readUTF(); client.Addt1("收到:"+"/n"); client.Addt1(" "+s+"/n"); } }catch(Exception e){ } } } |
多年的测试经验中,经常发现有这么一种现象:总有些提了的bug不能顺利的被修复。这些bug往往有4个走向: 1.在被发现的版本中最终被解决,但中途花费较多周折。
2.有计划的在后续的版本中被解决。
3.决定永远不修复,却变成潜在的炸弹,在后续版本中被迫修复。
4.决定永远不修复,至今为止也一直没有被修复。
近期对我们做过的项目做过一次较大的统计,统计严重程度中等及以上的缺陷,这四种走向第一种占到了50%左右,第二、三种各占20%,最后一种约占了10%。
这些没有被修改的bug带来的负面影响有:
1.大部分时候最终还得改了,是被迫改,项目组疲惫,在领导和客户那里都落不了好。
2.这些bug积累到一定数量,发现系统快不能要了,得大规模重构,重构的过程不要太痛苦,最后没准就推倒重来了(见过n个这样这样的案例了)。
3.拖得越久改起来越难,最近的一个案例是:某项目为了赶进度,使用了一个较低版本的底层组件,当时识别出低版本的底层组件特性有缺失,测试人员提出了功能bug,项目组决定忍了。一拖就是2年。结果项目很成功,越来越重要,与之交互的其它系统越来越多,但这个底层组件缺失特性的短板就越来越痛。最后不得不进行修复
工作(高版本组件替换),但发现由于代码耦合太紧,已经不是一个月两个月能搞定的事情了。大规模重构还是推到重来现在成了一个难题。
4.每天跟带着太多毛病的系统朝夕相对,是杀死所有干系人士气的慢性毒药。当你的潜意识认为你在做的东西是一团shit,还有毛激情?想一想破窗效应马上能够反应过来。
怎样降低大量bug长期遗留的现象呢?我有如下的一些建议:
1.提升内建质量。这句话高大上,内涵也很丰富,从软件架构,开发过程,各种技术应用等各方面都能够找到无数的提升点避免系统存在太多遗留bug,展开说真的要一本书了。从里边抽取出最重要的一条精神:bug被发现的越早,修改遇到的阻力越小。
2.定期bug扫除,这其实是测试应该主动提出来的事情,并且应该让这件事儿变成项目组的例行活动。其实如果做好了,乐趣还是很多的,效果也非常好。
3.如果是大型系统,或者项目群,很多bug是跨项目组的,这时候组织级的机制就要建立起来了,必要的时候需要跟考核制度挂钩。这样有一些三不管的重要bug才能被最终解决。
4.有些bug还真得睁一只眼闭一只眼了,约有10%的顽疾会这样。难改,影响范围有限。对这类bug最有效的办法是:挖雷难,我给它上边插个旗子让使用者离他远点儿好不好?有时候处理这些bug挺艺术的,运维,客服,售前,售后,都得长点儿心眼。
一、配置管理系统(Configuration Management System,CMS) 配置管理系统
项目管理系统的一个子系统。它由一系列正式的书面程序组成,该系统包含文件和跟踪系统,并明白了为核准和控制变更所需的批准层次。
配置管理系统是PMIS系统的子系统。该系统识别可交付成果状态、指导记录变更。在项目管理中,其功能是作为总体变更控制过程的一部分体现的。
1.配置对象:
配置的对象要么是可交付成果,要么是各个过程的技术规范。
2.配置管理的目的:
<1>建立一种先进的方法,以便规范地识别和提出对既定基准的变更,并评估变更的价值和有效性;
<2>通过分析各项变更的影响,为持续验证和改进项目创造机会;
<3>建立一种机制,以便项目管理团队规范地向有关干系人沟通变更的批准和否决情况。
3.配置管理的手段:
<1>识别并记录产品、成果、服务或部件的功能特征和物理特征;
<2>控制对上述特征的不论什么变更;
<3>记录并报告每一项变更及事实上施情况;
<4>支持对产品、成果或部件的审查,以确保其符合要求
注意:分清哪个是目的,哪个是手段。配置管理目的与手段的区分是一个常考点,也易错。
<1>配置识别。选择与识别配置项,从而为定义与核实产品配置、标志产品和文件、管理变更和明白责任提供基础。(相当于一个命名的规划过程)
<2>配置状态记录。包含已批准的配置识别清单、配置变更请求的状态和已批准的变更的实施状态。(相当于运行过程)
<3>配置核实与审计。确保配置文件所规定的功能要求都已实现。(相当于监控过程)
二、变更控制系统(Change Control System,CCS) 变更控制系统是是配置管理系统的一个子系统。
<1>通常作为配置管理系统的一个子系统。
<2>总体变更控制通过变更控制系统来完毕。
<3>一系列正式的书面程序,包含文档、跟踪系统和批准层次。
<4>不论什么变更请求都必须是正式提出的。
<5>该系统主要关注绩效測量基准的变更,如范围、进度、成本等。
变更控制详细工作过程遵循万能公式法则
三、配置管理系统与变更控制系统的差别
<1>变更控制系统是是配置管理系统的一个子系统,包括关系。
<2>关注的对象不同:
a.配置管理系统的对象:要么是可交付成果,要么是各个过程的技术规范。配置管理重点关注技术规范。
b.变更控制系统的管理对象:项目及产品基准(变更)。能够是产品的特性与性能(即产品范围),能够是为实现这些特性与功能的各种详细的项目工作(即项目范围)。变更控制系统重点关注基准的变更。
Warning: either you have JavaScriptdisabled or your browser does not support JavaScript. To work properly, thispage requires JavaScript to be enabled.
解决这个问题需要修改如何设置:
Internet选项——安全——自定义级别——脚本——
java小程序脚本和活动脚本目前是禁用状态,改为启用即可
apache tomcat/6.0..
提示HTTP status 404-
问题原因:服务启动没启动好
解决办法:停止jira服务,然后再启动即可
我们在开发服务时为了调试方便会在本地进行一个基本的模块
测试,你也可以认为是
集成测试,只不过你的
测试用例不会覆盖到80%以上,而是一些我们认为在开发时不是很放心的点才会编写适当的用例来测试它。
集成测试用例通常有多个执行上下文,对于我们开发人员来说我们的执行上下文通常都在本地,测试人员的上下文在测试环境中。开发人员的测试用来是不能够连接到其他环境中去的(当然视具体情况而定,有些用例很危险是不能够乱连接的,本文会讲如何解决),开发人员运行的集成测试用例所要访问的所有资源、服务都是在开发环境中的。这里依然存在但是,但是为了调试方便,我们还是需要能够在必要的时候连接到其他环境中去调试问题,为了能够真实的模拟出问题的环境、可真实的数据,我们需要能有一个这样的机制,在需要的时候我能够打开某个设置让其能够切换集成测试运行的环境上下文,其实说白了就是你所要连接的环境、数据源的连接地址。
本篇
文章我们将通过一个简单的实例来了解如何简单的处理这中情况,这其实基于对测试用来不断重构后的效果。
1 using System; 2 using Microsoft.VisualStudio.TestTools.UnitTesting; 3 4 namespace OrderManager.Test 5 { 6 using ProductService.Contract; 7 8 /// <summary> 9 /// Product service integration tests. 10 /// </summary> 11 [TestClass] 12 public class ProductServiceIntegrationTest 13 { 14 /// <summary> 15 /// service address. 16 /// </summary> 17 public const string ServiceAddress = "http://dev.service.ProductService/"; 18 19 /// <summary> 20 /// Product service get product by pid test. 21 /// </summary> 22 [TestMethod] 23 public void ProductService_GetProductByPid_Test() 24 { 25 var serviceInstance = ProductServiceClient.CreateClient(ServiceAddress); 26 var testResult = serviceInstance.GetProductByPid(0393844); 27 28 Assert.AreNotEqual(testResult, null); 29 Assert.AreEqual(testResult.Pid, 0393844); 30 } 31 } 32 } |
这是一个实际的集成测试用例代码,有一个当前测试类共用的服务地址,这个地址是DEV环境的,当然你也可以定义其他几个环境的服务地址,前提是环境是允许你连接的,那才有实际意义。
我们来看测试用例,它是一个查询方法测试用例,用来对ProductServiceClient.GetProductByPid服务方法进行测试,由于面向查询的操作是等幕的,不论我们查询多少次这个ID的Product,都不会对数据造成影响,但是如果我们测试的是一个更新或者删除就会带来问题。
在DEV环境中,测试更新、删除用例没有问题,但是如果你的机器是能够连接到远程某个生产或者PRD测试上时会带来一定的危险性,特别是在忙的时候,加班加点的干进度,你很难记住你当前的机器的host配置中是否还连接着远程的生产机器上,或者根本就不需要配置host就能够连接到某个你不应该连接的环境上。
这是目前的问题,那么我们如何解决这个问题呢 ,我们通过对测试代码进行一个简单的重构就可以避免由于连接到不该连接的环境中运行危险的测试用例。
其实很多时候,重构真的能够帮助我们找到出口,就好比俗话说的:"出口就在转角处“,只有不断重构才能够逐渐的保证项目的质量,而这种效果是很难得的。
提取抽象基类,对测试要访问的环境进行明确的定义。
1 namespace OrderManager.Test 2 { 3 public abstract class ProductServiceIntegrationBase 4 { 5 /// <summary> 6 /// service address. 7 /// </summary> 8 protected const string ServiceAddressForDev = "http://dev.service.ProductService/"; 9 10 /// <summary> 11 /// service address. 12 /// </summary> 13 protected const string ServiceAddressForPrd = "http://Prd.service.ProductService/"; 14 15 /// <summary> 16 /// service address. 17 /// </summary> 18 protected const string ServiceAddressTest = "http://Test.service.ProductService/"; 19 } 20 } |
对具体的测试类消除重复代码,加入统一的构造方法。
1 using System; 2 using Microsoft.VisualStudio.TestTools.UnitTesting; 3 4 namespace OrderManager.Test 5 { 6 using ProductService.Contract; 7 8 /// <summary> 9 /// Product service integration tests. 10 /// </summary> 11 [TestClass] 12 public class ProductServiceIntegrationTest : ProductServiceIntegrationBase 13 { 14 /// <summary> 15 /// product service client. 16 /// </summary> 17 private ProductServiceClient serviceInstance; 18 19 /// <summary> 20 /// Initialization test instance. 21 /// </summary> 22 [TestInitialize] 23 public void InitTestInstance() 24 { 25 serviceInstance = ProductServiceClient.CreateClient(ServiceAddressForDev/*for dev*/); 26 } 27 28 /// <summary> 29 /// Product service get product by pid test. 30 /// </summary> 31 [TestMethod] 32 public void ProductService_GetProductByPid_Test() 33 { 34 var testResult = serviceInstance.GetProductByPid(0393844); 35 36 Assert.AreNotEqual(testResult, null); 37 Assert.AreEqual(testResult.Pid, 0393844); 38 } 39 40 /// <summary> 41 /// Product service delete search index test. 42 /// </summary> 43 [TestMethod] 44 public void ProductService_DeleteProductSearchIndex_Test() 45 { 46 var testResult = serviceInstance.DeleteProductSearchIndex(); 47 48 Assert.IsTrue(testResult); 49 } 50 } 51 } |
消除重复代码后,我们需要加入对具体测试用例检查是否能够连接到某个环境中去。我加入了一个DeleteProductSearchIndex测试用例,该用例是用来测试删除搜索索引的,这个测试用例只能够在本地DEV环境中运行(你可能觉得这个删除接口不应该放在这个服务里,这里只是举一个例子,无需纠结)。
为了能够有一个检查机制能提醒开发人员你目前连接的地址是哪一个,我们需要借助于测试上下文。
重构后,我们看一下现在的测试代码结构。
1 using System; 2 using Microsoft.VisualStudio.TestTools.UnitTesting; 3 4 namespace OrderManager.Test 5 { 6 using ProductService.Contract; 7 8 /// <summary> 9 /// Product service integration tests. 10 /// </summary> 11 [TestClass] 12 public class ProductServiceIntegrationTest : ProductServiceIntegrationBase 13 { 14 /// <summary> 15 /// product service client. 16 /// </summary> 17 private ProductServiceClient serviceInstance; 18 19 /// <summary> 20 /// Initialization test instance. 21 /// </summary> 22 [TestInitialize] 23 public void InitTestInstance() 24 { 25 serviceInstance = ProductServiceClient.CreateClient(ServiceAddressForPrd/*for dev*/); 26 27 this.CheckCurrentTestCaseIsRun(this.serviceInstance);//check current test case . 28 } 29 30 /// <summary> 31 /// Product service get product by pid test. 32 /// </summary> 33 [TestMethod] 34 public void ProductService_GetProductByPid_Test() 35 { 36 var testResult = serviceInstance.GetProductByPid(0393844); 37 38 Assert.AreNotEqual(testResult, null); 39 Assert.AreEqual(testResult.Pid, 0393844); 40 } 41 42 /// <summary> 43 /// Product service delete search index test. 44 /// </summary> 45 [TestMethod] 46 public void ProductService_DeleteProductSearchIndex_Test() 47 { 48 var testResult = serviceInstance.DeleteProductSearchIndex(); 49 50 Assert.IsTrue(testResult); 51 } 52 } 53 } |
我们加入了一个很重要的测试实例运行时方法InitTestInstance,该方法会在测试用例每次实例化时先执行,在方法内部有一个用来检查当前测试用例运行的环境
this.CheckCurrentTestCaseIsRun(this.serviceInstance);//check current test case .,我们转到基类中。
1 using System; 2 using Microsoft.VisualStudio.TestTools.UnitTesting; 3 4 namespace OrderManager.Test 5 { 6 public abstract class ProductServiceIntegrationBase 7 { 8 /// <summary> 9 /// service address. 10 /// </summary> 11 protected const string ServiceAddressForDev = "http://dev.service.ProductService/"; 12 13 /// <summary> 14 /// get service address. 15 /// </summary> 16 protected const string ServiceAddressForPrd = "http://Prd.service.ProductService/"; 17 18 /// <summary> 19 /// service address. 20 /// </summary> 21 protected const string ServiceAddressTest = "http://Test.service.ProductService/"; 22 23 /// <summary> 24 /// Test context . 25 /// </summary> 26 public TestContext TestContext { get; set; } 27 28 /// <summary> 29 /// is check is run for current test case. 30 /// </summary> 31 protected void CheckCurrentTestCaseIsRun(ProductService.Contract.ProductServiceClient testObject) 32 { 33 if (testObject.ServiceAddress.Equals(ServiceAddressForPrd))// Prd 环境,需要小心检查 34 { 35 if (this.TestContext.TestName.Equals("ProductService_DeleteProductSearchIndex_Test")) 36 Assert.IsTrue(false, "当前测试用例连接的环境为PRD,请停止当前用例的运行。"); 37 } 38 else if (testObject.ServiceAddress.Equals(ServiceAddressTest))//Test 环境,检查约定几个用例 39 { 40 if (this.TestContext.TestName.Equals("ProductService_DeleteProductSearchIndex_Test")) 41 Assert.IsTrue(false, "当前测试用例连接的环境为TEST,为了不破坏TEST环境,请停止用例的运行。"); 42 } 43 } 44 } 45 } |
在检查方法中我们使用简单的判断某个用例不能够在PRD、TEST环境下执行,虽然判断有点简单,但是在真实的项目中足够了,简单有时候是一种设计思想。我们运行所有的测试用例,查看各个状态。
一目了然,更为重要的是它不会影响你对其他用例的执行。当你在深夜12点排查问题的时候,你很难控制自己的眼花、体虚导致的用例执行错误带来的大问题,甚至是无法挽回的的错误。
Andoird的SQLiteOpenHelper类中有一个onUpgrade方法。帮助文档中只是说当
数据库升级时该方法被触发。经过实践,解决了我一连串的疑问:
1. 帮助文档里说的“数据库升级”是指什么?
你开发了一个程序,当前是1.0版本。该程序用到了数据库。到1.1版本时,你在数据库的某个表中增加了一个字段。那么软件1.0版本用的数据库在软件1.1版本就要被升级了。
2. 数据库升级应该注意什么?
软件的1.0版本升级到1.1版本时,老的数据不能丢。那么在1.1版本的程序中就要有地方能够检测出来新的软件版本与老的数据库不兼容,并且能够 有办法把1.0软件的数据库升级到1.1软件能够使用的数据库。换句话说,要在1.0软件的数据库的那个表中增加那个字段,并赋予这个字段默认值。
3. 程序如何知道数据库需要升级?
SQLiteOpenHelper类的构造函数有一个参数是int version,它的意思就是指数据库版本号。比如在软件1.0版本中,我们使用SQLiteOpenHelper访问数据库时,该参数为1,那么数据库版本号1就会写在我们的数据库中。
到了1.1版本,我们的数据库需要发生变化,那么我们1.1版本的程序中就要使用一个大于1的整数来构造SQLiteOpenHelper类,用于访问新的数据库,比如2。
当我们的1.1新程序读取1.0版本的老数据库时,就发现老数据库里存储的数据库版本是1,而我们新程序访问它时填的版本号为2,系统就知道数据库需要升级。
4. 何时触发数据库升级?如何升级?
当系统在构造SQLiteOpenHelper类的对象时,如果发现版本号不一样,就会自动调用onUpgrade函数,让你在这里对数据库进行升级。根据上述场景,在这个函数中把老版本数据库的相应表中增加字段,并给每条记录增加默认值即可。
新版本号和老版本号都会作为onUpgrade函数的参数传进来,便于开发者知道数据库应该从哪个版本升级到哪个版本。
升级完成后,数据库会自动存储最新的版本号为当前数据库版本号。