2019独角兽企业重金招聘Python工程师标准>>>
测试驱动开发的意义
传统开发方法
传统的开发方法的流程是:
需求分析 -> 设计 -> 编写代码 ->测试 ->回归测试 -> 修改BUG->回顾测试....... ->上线 ->修改BUG。
从整个流程可以看出来传统开发方法的开发存在这些问题:
1.不能满足客户需求。
需求分析阶段缺乏测试和验证,导致产品需求不正确。
开发阶段开发出来的东西与需求不符,导致返工。
......
2.糟糕的代码质量。
在缺陷中苦苦挣扎,代码维护困难,开发进度缓慢...........
3.开发周期长。
由于项目流程的前面质量不过关,后面必须用十倍百倍的工作量来弥补。不断的返工,不断的修改、回归测试将我们的项目开发周期拉长了。
4.上线后的质量问题降低了用户的信任。
软件上线后,用户在使用过程中遇到的众多BUG让用户很郁闷。。。。。。。。用户对软件的信任度大大降低。
测试驱动开发方法
我们使用了测试驱动开发之后,整个软件开发的流程编程了这样:
需求分析 -> 设计 ->设计测试用例 ->编写测试用例 -> 编写代码 ->执行测试 ->重构代码->执行测试 ->测试 -> 修改BUG->上线
测试驱动的好处
1.不用长时间调试代码。
2.对自己写的代码更有信心。
3.回归测试更快了。
4.单元测试代码是最好的文档。
5.代码质量更高了。
在java web项目中的实践
本案例中是以我们自己的实际开发框架为例实现的单元测试。我们的开发框架是集成了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 } |
当然一个完整的登录功能,测试用例远不止这些,我们这里只选择三个测试用例来演示一下。
Action单元测试
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 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 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 login(String username, String password) {
Map result = new HashMap();
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层的代码实现最终的业务逻辑。
Service单元测试
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 login(String username, String password) {
Map result = new HashMap();
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 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 result = userAction.login(username, password);
Assert.assertNotNull(result);
Assert.assertEquals(Integer.valueOf(1), result.get("result"));
}
}
这个时候在测试的时候为了避免service的实现影响Action代码的测试,则需要使用模拟对象替换Service方法。
调整为这样的代码之后则执行通过了。
3 .编写Service单元测试
service层单元测试的方法与Action层类似,若此层代码中出现了对Dao或者其它远程服务的调用,则测试的时候也应该用模拟对象去模拟结果。