测试驱动开发实践——重构篇
1、写单元测试使他亮红灯
2、写代码使测试变成绿灯
3、重构代码
接下来我们需要开始重构了,大家有可能会问,为什么需要重构,什么时候开始重构。
对与为什么需要重构,其实就是为了使代码结构清晰,去除一些重复的代码,比如我们执行sql语句操作,我们起初这样写:
Code 1private connStr="server=.;database=TestDB;uid=sa;pwd=123" 2public int Add(string loginName) 3{ 4 int count = 0; 5 using (SqlConnection conn = new SqlConnection(connStr)) 6 { 7 conn.Open(); 8 SqlCommand cmd = new SqlCommand("insert(loginName) value('" + loginName + "')", conn); 9 count = cmd.ExecuteNonQuery(); 10 cmd.Dispose(); 11 conn.Close(); 12 } 13 return count; 14} 15 16public int Delete(string loginName) 17{ 18 int count = 0; 19 using (SqlConnection conn = new SqlConnection(connStr)) 20 { 21 conn.Open(); 22 SqlCommand cmd = new SqlCommand("delete from LoginUsers where loginName='" + loginName + "'", conn); 23 count = cmd.ExecuteNonQuery(); 24 cmd.Dispose(); 25 conn.Close(); 26 } 27 return count; 28} |
1private int ExecuteSql(string sql) 2{ 3 int count = 0; 4 using (SqlConnection conn = new SqlConnection(connStr)) 5 { 6 conn.Open(); 7 SqlCommand cmd = new SqlCommand(sql, conn); 8 count = cmd.ExecuteNonQuery(); 9 cmd.Dispose(); 10 conn.Close(); 11 } 12 return count; 13} 14public int Add(string loginName) 15{ 16 return ExecuteSql("insert(loginName) value('" + loginName + "')"); 17} 18public int Delete(string loginName) 19{ 20 return ExecuteSql("delete from LoginUsers where loginName='" + loginName + "'"); 21} |
这样重构完之后,代码是不是清晰了很多呢?
那什么时候又开始重构呢,我们在觉得代码重复性太大了,层次结构混乱了等等(就是传说中的有坏味道的代码)都可以重构,有测试在,还需要怕代码改错吗?
接下来我们就接着前一篇文章的用例按照 设计模式原则进行重构(单一职责原则,接口隔离原则,依赖倒置原则等)
那么我们回头看一下这个登陆的方法里,包含了验证和数据操作的代码,根据单一职责原则,我们需要把他们放在不同的类中,就有了如下代码:
1public interface IEmployeeService 2{ 3 bool Login(string loginName, string password); 4 bool ValidateLoginName(string loginName); 5} |
在上面代码中我们定义一个服务层接口,又把验证单独拿出来做了一个方法。
下面是对服务层的实现:
1public class EmployeeService : IEmployeeService2{
3 private IEmployeeDataAccess _empDAO;
4
5 public EmployeeService(IEmployeeDataAccess empDAO)
6 {
7
8 _empDAO = empDAO;
9 }
10
11 public bool ValidateLoginName(string loginName)
12 {
13 return !string.IsNullOrEmpty(loginName);
14 }
15
16 /**//// <summary>
17 /// 员工登陆
18 /// </summary>
19 /// <param name="loginName">登陆名</param>
20 /// <param name="password">密码</param>
21 /// <returns></returns>
22 public bool Login(string loginName, string password)
23 {
24 if (ValidateLoginName(loginName))
25 {
26 if (_empDAO.GetCount(loginName, password) > 0)
27 {
28 return true;
29 }
30 }
31
32 return false;
33 }
上面服务层我们可以看出,我们是调用了数据层接口的实现,而不是直接调用数据层的操作,这样的使服务层和数据层的关系进行解偶,服务层不是直接依赖于数据层,而是依赖于数据层接口,这样有什么好处呢?
比如我们现在数据层的实现是和sqlserver进行交互的,下次要与xml进行交互了,那怎么办呢,那我们只要实现一个与xml数据交互的数据层就可以了,其他代码也就不用修改了。
其实模式就是为了 变化 而准备的,假如项目真要交付了,什么事都没有了,我们还搞什么模式呢,你觉得呢?
这里有点跑题了,接下来我们再回到主题,数据接口的定义如下:
1public interface IEmployeeDataAccess 2{ 3 int GetCount(string loginName, string password); 4} |
数据层我这里就形式一下了:
1public class EmployeeAccess : IEmployeeDataAccess 2{ 3 IEmployeeDataAccess 成员#region IEmployeeDataAccess 成员 4 5 public int GetCount(string loginName, string password) 6 { 7 throw new NotImplementedException(); 8 } 9 10 #endregion 11} |
最后我们还需要修改我们的测试用例,使他继续运行通过,最后的测试用例如下:
1[TestFixture] 2public class EmployeeServiceTest 3{ 4 private IEmployeeService empService; 5 6 [TestFixtureSetUp] 7 public void Init() 8 { 9 var da = new Mock<IEmployeeDataAccess>(); 10 da.Setup(dd => dd.GetCount(It.Is<string>(s => s == "admin"), It.Is<string>(s => s == "pwd"))).Returns(1); 11 empService = new EmployeeService(da.Object); 12 } 13 14 [Test] 15 public void LoginTest() 16 { 17 Assert.IsTrue(empService.Login("admin", "pwd"), "登陆失败"); 18 } 19 20 [Test] 21 public void ValidateNullLoginName() 22 { 23 Assert.IsTrue(!empService.ValidateLoginName(null), "用户为null验证测试失败"); 24 } 25 26 [Test] 27 public void ValidateEmptyLoginName() 28 { 29 Assert.IsTrue(!empService.ValidateLoginName(""), "用户为Empty验证测试失败"); 30 } 31} |
这里重构就到这里了,这里服务层测试使用到了moq的mock框架可以在 http://code.google.com/p/moq 下到,所以这里用了mock模拟数据层进行了测试,这个框架对于分层开发测试非常好,在数据层没有写完的时候,我们就可以模拟数据层提供数据,直接对服务层进行测试。