关键字:Unit Test
1. 基本概念 1)什么是单元测试(Unit Test)?
单元测试是在软件开发过程中要进行的最低级别的测试活动,在单元测试活动中,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。在程序设计过程中会有许多种测试,单元只是其中的一种,单元测试并不能保证程序是完美无缺的,但是在所有的测试中,单元测试是第一个环节,也是最重要的一个环节。单元测试是一种由程序员自行测试的工作。简单点说,单元测试就是测试代码撰写者依据其所设想的方式执行是否产生了预期的结果。
2)什么是测试驱动开发(TDD, Test-Driven Development)?
测试驱动开发以测试作为开发过程的中心,它要求在编写任何代码之前,首先编写用于定义产品代码行为的测试,而编写的产品代码又以使测试通过为目标。测试驱动开发要求测试可以完全自动地运行,在代码进行重构前必须运行测试。
它的基本做法如下:
1. 写一个测试程序。
2. 让程序编译通过。
3. 运行测试程序,发现不能运行。
4. 让测试程序可以运行。
5. 消除重复设计,优化设计结构。
2. 为什么要采用单元测试
1) 减少程序的Bug
要减少软件中的错误数目,方法之一就是拥有一个专业的测试组,其工作就是尽一切可能使软件崩溃。不幸的是,如果拥有测试组,那么即使是经验丰富的开发人员,也会倾向于花费较少的时间来保证代码的可靠性。
软件界有一句俗语:“开发人员不应该测试他们自己的代码”。这是因为开发人员对自己的代码了如指掌,他们很清楚如何采用适当的方法对代码进行测试。尽管这句俗语很有道理,但却忽略了非常重要的一点 - 如果开发人员不对自己的代码进行测试,又如何知道代码能否按照预期的方式运行?
简单说来,他们根本无从得知。开发人员编写那种运行不正常或只在某些情况下运行正常的代码是一个严重的问题。他们通常只测试代码能否在很少的情况下正常运行,而不是验证代码能够在所有情况下均正常运行。
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"
);
}
测试修改时,我们修改前和修改后都写了测试代码,这样就可以保证修改有效了。
从上面的方法可以看出,单元测试代码非常好写,而通过测试代码,要求开发人员对自己程序的测试结果有明确的认识。而测试方法设计的好坏与否直接影响到你的测试是否能够找出真正的错误。一般编写测试代码时,至少要把正常的情况和边界情况都测试一遍。
当然如果一些方法,你有绝对的把握,认为不写测试代码都可以保证是对的。这部分方法你也可以跳过不写
延伸阅读
文章来源于领测软件测试网 https://www.ltesting.net/