传统的开发方法的流程是:
需求分析 -> 设计 -> 编写代码 ->测试 ->回归测试 -> 修改BUG->回顾测试....... ->上线 ->修改BUG。
从整个流程可以看出来传统开发方法的开发存在这些问题:
1.不能满足客户需求。
需求分析阶段缺乏测试和验证,导致产品需求不正确。
开发阶段开发出来的东西与需求不符,导致返工。
......
2.糟糕的代码质量。
在缺陷中苦苦挣扎,代码维护困难,开发进度缓慢...........
3.开发周期长。
由于项目流程的前面质量不过关,后面必须用十倍百倍的工作量来弥补。不断的返工,不断的修改、回归测试将我们的项目开发周期拉长了。
4.上线后的质量问题降低了用户的信任。
软件上线后,用户在使用过程中遇到的众多BUG让用户很郁闷。。。。。。。。用户对软件的信任度大大降低。
我们使用了测试驱动开发之后,整个软件开发的流程编程了这样:
需求分析 -> 设计 ->设计测试用例 ->编写测试用例 -> 编写代码 ->执行测试 ->重构代码->执行测试 ->测试 -> 修改BUG->上线
2.对自己写的代码更有信心。
3.回归测试更快了。
4.单元测试代码是最好的文档。
5.代码质量更高了。
本案例中是以我们自己的实际开发框架为例实现的单元测试。我们的开发框架是集成了Spring + SpringMVC + MyBatis框架而成的。所以本实例的单元测试代码是基于这些框架实现的。
本案例以一个登录的用例为实例,这是经典的案例了,相当于“hello,world”了。
访问路径是:/user/login.json
案例1 |
输入空的用户名和密码 |
参数 |
username:"" password:"" |
返回值 json格式的数据 |
{ "result":0, "error":"用户名和密码都不允许空" } |
|
|
案例2 |
输入错误的用户名和密码 |
参数 |
username:"user" password:"error" |
返回值 json格式的数据 |
{ "result":0, "error":"用户名和密码错误" } |
|
|
案例3 |
输入正确的用户名和密码 |
参数 |
username:"user" password:"password" |
返回值 json格式的数据 |
{ "result":1 } |
当然一个完整的登录功能,测试用例远不止这些,我们这里只选择三个测试用例来演示一下。
1.编写Action层的单元测试
package cn.bidlink.yuecai.plan.action; import java.util.Map; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class UserActionTest { private UserAction userAction; @Before public void setup(){ userAction = new UserAction(); } /** * 测试登录失败的情况。 */ @Test public void testLoginFaild(){ Map<String, Object> result = userAction.login("", ""); Assert.assertNotNull(result); Assert.assertEquals(Integer.valueOf(0), result.get("result")); Assert.assertEquals("用户名和密码都不允许空", result.get("error")); result = userAction.login("user", "error"); Assert.assertNotNull(result); Assert.assertEquals(Integer.valueOf(0), result.get("result")); Assert.assertEquals("用户名和密码错误", result.get("error")); } /** * 测试登录失败的情况。 */ @Test public void testLoginSucess(){ Map<String, Object> result = userAction.login("user", "password"); Assert.assertNotNull(result); Assert.assertEquals(Integer.valueOf(1), result.get("result")); } }
当我们代码还没有实现的时候,测试用例肯定会执行失败。
2.实现Action方法代码
我们为了让单元测试通过,我们要继续实现代码。编写的实现代码如下:
package cn.bidlink.yuecai.plan.action; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping(value = "/user") public class UserAction { @RequestMapping(value = "login") @ResponseBody public Map<String, Object> login(String username, String password) { Map<String, Object> result = new HashMap<String, Object>(); if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ result.put("result", Integer.valueOf(0)); result.put("error", "用户名和密码都不允许空"); return result; } else if(username.equals("user")){ if (password.equals("error")){ result.put("result", Integer.valueOf(0)); result.put("error", "用户名和密码错误"); return result; } else if (password.equals("password")){ result.put("result", Integer.valueOf(1)); return result; } } return result; } }
单元测试结果是:
如果安装了覆盖率检查工具,还可以查看单元测试的覆盖率情况。
这样我们Action层的代码编写完毕。但是大家可以看出来,我们真实的实现肯定不是这样的,我们的用户密码校验肯定是要与服务端存储的数据进行验证的,接下来还要调用Service和dao层的代码实现最终的业务逻辑。
1.修改Action层代码
修改UserAction的实现方式,改为代用Servcie方法实现登录。
package cn.bidlink.yuecai.plan.action; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import cn.bidlink.yuecai.plan.service.UserService; @Controller @RequestMapping(value = "/user") public class UserAction { @Autowired private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } @RequestMapping(value = "login") @ResponseBody public Map<String, Object> login(String username, String password) { Map<String, Object> result = new HashMap<String, Object>(); if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ result.put("result", Integer.valueOf(0)); result.put("error", "用户名和密码都不允许空"); return result; } if(userService.login(username, password)){ result.put("result", Integer.valueOf(1)); return result; } else{ result.put("result", Integer.valueOf(0)); result.put("error", "用户名和密码错误"); return result; } } }
这个时候执行单元测试发现报空指针异常了,需要调整单元测试。
package cn.bidlink.yuecai.plan.action; import java.util.Map; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import cn.bidlink.framework.test.action.AbstractActionTests; import cn.bidlink.yuecai.plan.service.UserService; public class UserActionTest extends AbstractActionTests{ @InjectMocks private static UserAction userAction; @Mock private UserService userService; @BeforeClass public static void setup(){ userAction = new UserAction(); } /** * 测试登录失败的情况。 */ @Test public void testLoginFaild(){ Map<String, Object> result = userAction.login("", ""); Assert.assertNotNull(result); Assert.assertEquals(Integer.valueOf(0), result.get("result")); Assert.assertEquals("用户名和密码都不允许空", result.get("error")); /** * 设置预期结果,返回false。 */ String username = "user"; String password = "error"; Mockito.when(userService.login(username, password)).thenReturn(false); result = userAction.login(username, password); Assert.assertNotNull(result); Assert.assertEquals(Integer.valueOf(0), result.get("result")); Assert.assertEquals("用户名和密码错误", result.get("error")); } /** * 测试登录失败的情况。 */ @Test public void testLoginSucess(){ /** * 设置预期结果,返回false。 */ String username = "user"; String password = "password"; Mockito.when(userService.login(username, password)).thenReturn(true); Map<String, Object> result = userAction.login(username, password); Assert.assertNotNull(result); Assert.assertEquals(Integer.valueOf(1), result.get("result")); } }
这个时候在测试的时候为了避免service的实现影响Action代码的测试,则需要使用模拟对象替换Service方法。
调整为这样的代码之后则执行通过了。
3 .编写Service单元测试
service层单元测试的方法与Action层类似,若此层代码中出现了对Dao或者其它远程服务的调用,则测试的时候也应该用模拟对象去模拟结果。