關鍵字:Unit Test
1. 基本概念 1)什么是單元測試(Unit Test)?
單元測試是在軟件開發過程中要進行的最低級別的測試活動,在單元測試活動中,軟件的獨立單元將在與程序的其他部分相隔離的情況下進行測試。在程序設計過程中會有許多種測試,單元只是其中的一種,單元測試并不能保證程序是完美無缺的,但是在所有的測試中,單元測試是第一個環節,也是最重要的一個環節。單元測試是一種由程序員自行測試的工作。簡單點說,單元測試就是測試代碼撰寫者依據其所設想的方式執行是否產生了預期的結果。
2)什么是測試驅動開發(TDD, Test-Driven Development)?
測試驅動開發以測試作為開發過程的中心,它要求在編寫任何代碼之前,首先編寫用于定義產品代碼行為的測試,而編寫的產品代碼又以使測試通過為目標。測試驅動開發要求測試可以完全自動地運行,在代碼進行重構前必須運行測試。
它的基本做法如下:
1. 寫一個測試程序。
2. 讓程序編譯通過。
3. 運行測試程序,發現不能運行。
4. 讓測試程序可以運行。
5. 消除重復設計,優化設計結構。
2. 為什么要采用單元測試
1) 減少程序的Bug
要減少軟件中的錯誤數目,方法之一就是擁有一個專業的測試組,其工作就是盡一切可能使軟件崩潰。不幸的是,如果擁有測試組,那么即使是經驗豐富的開發人員,也會傾向于花費較少的時間來保證代碼的可靠性。
軟件界有一句俗語:“開發人員不應該測試他們自己的代碼”。這是因為開發人員對自己的代碼了如指掌,他們很清楚如何采用適當的方法對代碼進行測試。盡管這句俗語很有道理,但卻忽略了非常重要的一點 - 如果開發人員不對自己的代碼進行測試,又如何知道代碼能否按照預期的方式運行?
簡單說來,他們根本無從得知。開發人員編寫那種運行不正;蛑辉谀承┣闆r下運行正常的代碼是一個嚴重的問題。他們通常只測試代碼能否在很少的情況下正常運行,而不是驗證代碼能夠在所有情況下均正常運行。
2) 提高開發速度
一般大家都會認為單元測試會浪費時間,這是一個誤區。一旦編碼完成,開發人員總是會迫切希望進行軟件的集成工作,這樣他們就能夠看到實際的系統開始啟動工作了。 這在外表上看來是一項明顯的進步,而象單元測試這樣的活動也許會被看作是通往這個階段點的道路上的障礙, 推遲了對整個系統進行聯調這種真正有意思的工作啟動的時間。
在這種開發步驟中,真實意義上的進步被外表上的進步取代了。系統能夠正常工作的可能性是很小的,更多的情況是充滿了各式各樣的Bug。在實踐中,這樣一種開發步驟常常會導致這樣的結果:軟件甚至無法運行。更進一步的結果是大量的時間將被花費在跟蹤那些包含在獨立單元里的簡單的Bug上面,在個別情況下,這些Bug也許是瑣碎和微不足道的,但是總的來說,他們會導致在軟件集成為一個系統時增加額外的工期, 而且當這個系統投入使用時也無法確保它能夠可靠運行。
在實踐工作中,進行了完整計劃的單元測試和編寫實際的代碼所花費的精力大致上是相同的。一旦完成了這些單元測試工作,很多Bug將被糾正,在確信他們手頭擁有穩定可靠的部件的情況下,開發人員能夠進行更高效的系統集成工作。這才是真實意義上的進步,所以說完整計劃下的單元測試是對時間的更高效的利用。而調試人員的不受控和散漫的工作方式只會花費更多的時間而取得很少的好處。
發現軟件錯誤的情況有很多:
1、由首次編寫代碼的開發人員發現。
2、由嘗試運行代碼的開發人員發現。
3、由組中的其他開發人員或測試人員發現。
4、作為產品大規模測試的一部分。
5、由最終用戶發現。
如果在第一種情況下發現軟件錯誤,則修復錯誤比較容易,成本也很低。情況越靠后,修復軟件錯誤的成本就越高;修復一個由最終用戶發現的軟件錯誤可能要耗費 100 或 1000 倍的成本。更不用說用戶通常因為軟件錯誤導致工作無法繼續,而一直等到下一個版本才能解決問題。
如果開發人員能夠在編寫代碼期間發現所有的軟件錯誤,那就再好不過了。為此,您必須編寫能在編寫代碼時運行的測試。有一種很不錯的方法,它恰好可以做到這一點。
經驗表明一個盡責的單元測試方法將會在軟件開發的某個階段發現很多的Bug,并且修改它們的成本也很低。在軟件開發的后期階段,Bug的發現并修改將會變得更加困難,并要消耗大量的時間和開發費用。無論什么時候作出修改都要進行完整的回歸測試,在生命周期中盡早地對軟件產品進行測試將使效率和質量得到最好的保證。 在提供了經過測試的單元的情況下,系統集成過程將會大大地簡化。開發人員可以將精力集中在單元之間的交互作用和全局的功能實現上,而不是陷入充滿很多Bug的單元之中不能自拔。
3) 使程序代碼更整潔,優化程序的設計
只有自動的單元測試程序失敗時,我們才會去重寫代碼,在測試驅動開發中,要求我們對程序不停的重構,通過重構,我們可以優化程序的結構設計,消除程序中潛在的錯誤。同時,為了能夠使自己的程序可以很方便的進行測試,開發人員就需要很好地考慮程序的設計,極限編程的方法說可以不需要設計就開始編碼,但實際上,它在編寫代碼的過程中每時每刻都為了方便的進行和通過測試而在優化自己的設計。它實際上是把開始階段很大很抽象的設計分散到你編寫的每個方法中。因此他們會說好設計最后會自然而然的出現。
4) 編寫單元測試代碼的過程實際上就是設計程序的過程。
在編寫單元測試代碼時,我們實際上是在思考我們的程序根據預期會返回什么結果,它實際上就是程序設計的過程。而通過重構過程,我們可以對這些設計進行很好的優化。
3. 如何進行單元測試
下面我通過一個簡單的例子來演示一下在應用開發的過程中如何編寫單元測試。在演示之前,我們已經安裝了Nunit或者VSNunit等單元測試工具。
我們的程序中有一個Users來,它對應的是數據庫中的一個Users表,其代碼如下:
using System;
namespace DB
{
public class users
{
public users()
{
}
private System.String _Password;
public System.String Password
{
get { return _Password; }
set { _Password = value; }
}
private System.DateTime _LastLogon;
public System.DateTime LastLogon
{
get { return _LastLogon; }
set { _LastLogon = value; }
}
private System.String _Name;
public System.String Name
{
get { return _Name; }
set { _Name = value; }
}
private System.String _LogonID;
public System.String LogonID
{
get { return _LogonID; }
set { _LogonID = value; }
}
private System.String _EmailAddress;
public System.String EmailAddress
{
get { return _EmailAddress; }
set { _EmailAddress = value; }
}
}
}
我們使用另外一個類EntityControl來通過ORM的方法把這個類中的數據通過增刪改的方法與數據庫中的數據進行同步,這里我們只需要知道它有這個功能就足夠。
using System;
using System.Reflection;
using System.Data;
using System.Data.SqlClient;
using NHibernate;
using NHibernate.Type;
using NHibernate.Cfg;
using NHibernate.Dialect;
using NHibernate.Tool.hbm2ddl;
using System.Collections;
namespace DB
{
///
/// Summary description for UsersControl.
///
public class EntityControl
{
private static EntityControl entity;
private static ISessionFactory sessions;
private static Configuration cfg;
private static Dialect dialect;
public static EntityControl CreateControl()
{
if (entity == null)
{
BuildSessionFactory();
if (entity == null)
entity = new EntityControl();
}
return entity;
}
private static void BuildSessionFactory()
{
ExportSchema( new string[] { "users.hbm.xml"
, "Department.hbm.xml"
, "Employee.hbm.xml"
} , false);
}
public void AddEntity(object entity)
{
ISession s = sessions.OpenSession();
ITransaction t = s.BeginTransaction();
try
{
s.Save(entity);
t.Commit();
}
catch(Exception e)
{
t.Rollback();
throw e;
}
finally
{
s.Close();
}
}
public void UpdateEntity(object entity,object key)
{
ISession s = sessions.OpenSession();
ITransaction t = s.BeginTransaction();
try
{
s.Update(entity,key);
t.Commit();
}
catch(Exception e)
{
t.Rollback();
throw e;
}
finally
{
s.Close();
}
}
public void DeleteEntity(object entity)
{
ISession s = sessions.OpenSession();
ITransaction t = s.BeginTransaction();
try
{
s.Delete(entity);
t.Commit();
}
catch(Exception e)
{
t.Rollback();
throw e;
}
finally
{
s.Close();
}
}
public object GetEntity(System.Type theType, object id)
{
object obj;
ISession s = sessions.OpenSession();
ITransaction t = s.BeginTransaction();
obj = s.Load( theType, id);
t.Commit();
s.Close();
return obj;
}
public IList GetEntities(string query)
{
IList lst;
ISession s = sessions.OpenSession();
ITransaction t = s.BeginTransaction();
lst = s.Find(query);
t.Commit();
s.Close();
return lst;
}
public IList GetEntities(string query, object value, IType type)
{
IList lst;
ISession s = sessions.OpenSession();
ITransaction t = s.BeginTransaction();
lst = s.Find(query,value,type);
t.Commit();
s.Close();
return lst;
}
#region "Schema deal"
private static void ExportSchema(string[] files)
{
ExportSchema(files, true);
}
private static void ExportSchema(string[] files, bool exportSchema)
{
cfg = new Configuration();
for (int i=0; i
{
cfg.AddResource("DB." + files[i], Assembly.Load("DB"));
}
if(exportSchema) new SchemaExport(cfg).Create(true, true);
sessions = cfg.BuildSessionFactory( );
dialect = NHibernate.Dialect.Dialect.GetDialect();
}
///
/// Drops the schema that was built with the TestCase’s Configuration.
///
private static void DropSchema()
{
new SchemaExport(cfg).Drop(true, true);
}
private static void ExecuteStatement(string sql)
{
ExecuteStatement(sql, true);
}
private static void ExecuteStatement(string sql, bool error)
{
IDbConnection conn = null;
IDbTransaction tran = null;
try
{
if (cfg == null)
cfg = new Configuration();
NHibernate.Connection.IConnectionProvider prov = NHibernate.Connection.ConnectionProviderFactory.NewConnectionProvider(cfg.Properties);
conn = prov.GetConnection();
tran = conn.BeginTransaction();
IDbCommand comm = conn.CreateCommand();
comm.CommandText = sql;
comm.Transaction = tran;
comm.CommandType = CommandType.Text;
comm.ExecuteNonQuery();
tran.Commit();
}
catch(Exception exc)
{
if (tran != null)
tran.Rollback();
if (error)
throw exc;
}
finally
{
if (conn != null)
conn.Close();
}
}
#endregion
}
}
這兩個類提供的方法,我們可以從下面的類圖中看出來:

有了上面的背景知識,我們下面來看如何對這兩個遍寫單元測試代碼。
我們先建立一個類---UnitTest.cs,在這個類中,我們加入下面的名字空間:
using NUnit.Framework;
我們在類名前面加上一個Attribute—TestFixture,Nunit在看到這個后,就會把這個類當作單元測試的類來處理。
[TestFixture]
public class UnitTest
下面我們增加下面的代碼:
private EntityControl control;
[SetUp]
public void SetUp()
{
control = EntityControl.CreateControl();
}
[Setup]的作用就是在單元測試時,提供一些初始化的數據,在后面的測試方法中,我們就可以使用這個數據,好像類似于我們類中的Constructor.
下面我們來看如何測試增加用戶的方法:
[Test]
public void AddTest()
{
users user = new users();
user.LogonID = "1216";
user.Name = "xian city1";
user.EmailAddress = "tim.wang@grapecity.com1";
control.AddEntity(user);
users u2 =(users) control.GetEntity(typeof(users),user.LogonID);
Assert.IsTrue(
u2.Name.Equals("xian city1") &&
u2.EmailAddress.Equals("tim.wang@grapecity.com1")
);
Assert.IsFalse(
u2.Name.Equals("xian city") &&
u2.EmailAddress.Equals("tim.wang@grapecity.com")
);
}
在上面的測試方法中,我們首先定一個新用戶,然后用AddEntity把它增加到數據中,為了驗證我們增加到數據庫中的數據是否正確,我們通過GetEntity方法根據主鍵再把它取出來。通過與我們剛才輸入的數據進行比較來判斷是否正確,在這里測試的時候,我們進行了兩次測試,一個用正確的數據,一個用錯誤的數據,其目的是保證這個測試真正其到作用。
測試修改的方法和這個很類似:
[Test]
[Ignore("Finished Test")]
public void UpdateTest()
{
users u1 =(users) control.GetEntity(typeof(users),"112");
Assert.IsTrue(
u1.Password == "123" &&
u1.EmailAddress == "234"
);
u1.Password = "aaa";
u1.EmailAddress = "tim";
control.UpdateEntity(u1,"112");
Assert.IsFalse(
u1.Password == "123" ||
u1.EmailAddress == "234"
);
Assert.IsTrue(
u1.Password == "aaa" &&
u1.EmailAddress == "tim"
);
}
測試修改時,我們修改前和修改后都寫了測試代碼,這樣就可以保證修改有效了。
從上面的方法可以看出,單元測試代碼非常好寫,而通過測試代碼,要求開發人員對自己程序的測試結果有明確的認識。而測試方法設計的好壞與否直接影響到你的測試是否能夠找出真正的錯誤。一般編寫測試代碼時,至少要把正常的情況和邊界情況都測試一遍。
當然如果一些方法,你有絕對的把握,認為不寫測試代碼都可以保證是對的。這部分方法你也可以跳過不寫
延伸閱讀
文章來源于領測軟件測試網 http://www.kjueaiud.com/