最近在我们的项目中,进行了一次如何进行Test 的大讨论。
大家都知道,Test 分为UnitTest 和 Function Test, 关于Function Test, 我们大家都没有歧义, 认为现阶段使用WatiN 的自动化测试已经能够满足项目的基本需要, 大家的分歧主要就在于如何进行Unit Test.
很显然,Unit Test是在测试我们的项目的最小单元, 但是问题是在一个 Data Driven 的项目中, 很多的最小单元都会使用到数据库访问,于是 Unit Test 是否需要Touch DB 就成为了这次讨论的核心问题。 我们主要有两种观点
1. 数据库测试也是我们必要的测试一部分 ,因此我们有必要在Unit Test中对物理的数据库访问的函数进行测试。
2.我们的测试并不希望去访问数据库,因为我们的Unit Test的测试用例将近 上千, 这样在每次check in 之后, 运行所有的Unit Test 就会不现实
持第一种观点的同学认为,既然我们有Daily Build的策略,我们不需要每次的 check in 都触发Unit Test的case, 在daily build 中一起运行Unit Test, Coverage Test 和Function Test,将测试报表发到大家的信箱就可以了。
持第二种观点的同学认为,Unit Test是保证代码质量的最低要求,因此需要明每次Check In 都触发Unit Test 并且Unit Test 应该是没有执行次序的, 如果涉及到数据库访问,则必须需要复杂的 Build Script 以保证Unit Test的执行次序。
经过差不多一周的lunch discussion 我们基本上同意使用第二种方案。
但是第二种方案会带来一些新的挑战,如果进行Unit Test ,那些碰到数据库访问的方法, 或者要调用数据库访问的方法和类该如何测试?
在这里,我提出的解决方案是使用NMock 这个Open Source 的项目。
下面简单介绍一下这个 项目, 你可以在http://nmock.org/ 这个地址看到关于这个项目的详细介绍, 我举一个非常简单的例子。
我们的项目一般都会分为 数据层, 业务层和 展现层。
一个典型的数据层是这样的
namespace Q4.Core.Datalayer.Interface
{
public interface IUser
{
int UserId { get;set;}
string UserName { get;set;}
string FirstName { get;set;}
string LastName { get;set;}
}
}
namespace Q4.Core.Datalayer
{
public class User : IUser
{
}
}
典型的业务逻辑层,我们一般会添加用户访问的处理逻辑。
namespace Q4.DataAdapter.Interface
{
public interface IUserEntity
{
bool AddUser(string userName, string firstName, string lastName);
}
}
namespace Q4.DataAdapter
{
public class UserEntity :IDisposable,Interface.IUserEntity
{
private SqlConnection _conn;
private string _connectionString;
private static UserEntity _instance;
public static UserEntity Instance()
{
string connectionString = "";
_instance = new UserEntity(connectionString);
return _instance;
}
private UserEntity(string connectionString)
{
_connectionString = connectionString;
_conn = new SqlConnection(connectionString);
}
#region IDisposable Members
public void Dispose()
{
if (_conn != null)
{
_conn.Dispose();
}
}
#endregion
#region IUserEntity Members
public bool AddUser(string userName, string firstName, string lastName)
{
string sqlFormat = "INSERT INTO [MockUser]([UserName],[FirstName],[LastName])VALUES('{0}','{1}','{2}')";
string sql = string.Format(sqlFormat, userName, firstName, lastName);
using (SqlCommand cmd = new SqlCommand(sql))
{
cmd.Connection = _conn;
cmd.ExecuteNonQuery();
}
return true;
}
#endregion
}
}
在这一层的下面,我们会根据MVC 的架构再相应的搭建起来Controller 和 View. 这个我就不再浪费 大家的时间了。
如果我们希望在Controller中测试 AddUser 成功后, 我们希望将这个用户再添加到对应的用户组中,如果我们使用Mockup Object 的话,代码就类似于这样
namespace Q4.UnitTest
{
[TestFixture]
public class MockupTest
{
[Test]
public void TestAddUser()
{
Mockery mocks = new Mockery();
Q4.DataAdapter.Interface.IUserEntity userService = mocks.NewMock<Q4.DataAdapter.Interface.IUserEntity>();
Expect.Once
.On(userService)
.Method("AddUser")
.With("doufu@hotmail.com", "boke","Tang")
.Will(Return.Value(true));
Q4.DataAdapter.Interface.IUserEntity groupService = mocks.NewMock<Q4.DataAdapter.Interface.IGroupEntity>();
Expect.Once
.On(userService)
.Method("AddUserToGroup")
.With("doufu@hotmail.com", "Admin")
.Will(Return.Value(true));
bool bRet=userService.AddUser("robertx@q4websystems.com", "Robert", "Xue");
if(bRet)
{
bRet=groupService.AddUserToGroup("doufu@hotmail.com","Admin");
}
}
}
}
当然实际的项目中, 测试的代码不会这么简单,我们需要利用Hashtable 建立起一套内存中的数据结构 供 Unit Test 来调用。不过 我相信再引入NMock 以后, 对于Data Driven 的项目进行Unit Test可以提供一种有益的思路。
欢迎大家跟贴和我一起继续探讨。谢谢。