現象: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函數的參數傳進來,便于開發者知道數據庫應該從哪個版本升級到哪個版本。
升級完成后,數據庫會自動存儲最新的版本號為當前數據庫版本號。